From d8244f0cfde939ba4fe82c52f98f79e7763c3841 Mon Sep 17 00:00:00 2001 From: azure-sdk Date: Wed, 20 May 2026 18:09:55 +0000 Subject: [PATCH 1/6] Configurations: 'specification/keyvault/data-plane/Keys/tspconfig.yaml', API Version: 2026-03-01-preview, SDK Release Type: beta, and CommitSHA: '9dd262c196cd5d27bb01a6bf4d645d9c89cc0f02' in SpecRepo: 'https://github.com/Azure/azure-rest-api-specs' Pipeline run: https://dev.azure.com/azure-sdk/internal/_build/results?buildId=6328433 Refer to https://eng.ms/docs/products/azure-developer-experience/develop/sdk-release/sdk-release-prerequisites to prepare for SDK release. --- sdk/keyvault/azure-keyvault-keys/CHANGELOG.md | 4 + sdk/keyvault/azure-keyvault-keys/MANIFEST.in | 2 +- .../azure-keyvault-keys/_metadata.json | 10 +- .../apiview-properties.json | 12 +- .../azure/keyvault/keys/__init__.py | 44 +- .../azure/keyvault/keys/_client.py | 1010 -------- .../azure/keyvault/keys/_enums.py | 72 - .../azure/keyvault/keys/_generated/_client.py | 14 +- .../keys/_generated/_configuration.py | 9 +- .../keyvault/keys/_generated/_model_base.py | 1235 ---------- .../_generated/_operations/_operations.py | 554 ++++- .../keys/_generated/_operations/_patch.py | 12 +- .../azure/keyvault/keys/_generated/_patch.py | 12 +- .../keys/_generated/_serialization.py | 2050 ----------------- .../keys/_generated/_utils/model_base.py | 26 +- .../keys/_generated/_utils/serialization.py | 6 +- .../azure/keyvault/keys/_generated/_vendor.py | 25 - .../keyvault/keys/_generated/_version.py | 2 +- .../keyvault/keys/_generated/aio/_client.py | 14 +- .../keys/_generated/aio/_configuration.py | 9 +- .../_generated/aio/_operations/_operations.py | 448 +++- .../keys/_generated/aio/_operations/_patch.py | 12 +- .../keyvault/keys/_generated/aio/_patch.py | 12 +- .../keyvault/keys/_generated/aio/_vendor.py | 25 - .../keys/_generated/models/__init__.py | 10 + .../keyvault/keys/_generated/models/_enums.py | 28 +- .../keys/_generated/models/_models.py | 167 ++ .../keyvault/keys/_generated/models/_patch.py | 12 +- .../azure/keyvault/keys/_models.py | 659 ------ .../azure/keyvault/keys/_sdk_moniker.py | 7 - .../azure/keyvault/keys/_shared/__init__.py | 78 - .../azure/keyvault/keys/_shared/_polling.py | 136 -- .../keyvault/keys/_shared/_polling_async.py | 80 - .../_shared/async_challenge_auth_policy.py | 256 -- .../keys/_shared/async_client_base.py | 117 - .../keys/_shared/challenge_auth_policy.py | 270 --- .../keyvault/keys/_shared/client_base.py | 162 -- .../keyvault/keys/_shared/http_challenge.py | 186 -- .../keys/_shared/http_challenge_cache.py | 93 - .../azure/keyvault/keys/_version.py | 6 - .../azure/keyvault/keys/aio/__init__.py | 7 - .../azure/keyvault/keys/aio/_client.py | 1017 -------- .../azure/keyvault/keys/crypto/__init__.py | 32 - .../azure/keyvault/keys/crypto/_client.py | 577 ----- .../azure/keyvault/keys/crypto/_enums.py | 67 - .../keys/crypto/_internal/__init__.py | 32 - .../keys/crypto/_internal/_internal.py | 131 -- .../keys/crypto/_internal/algorithm.py | 78 - .../crypto/_internal/algorithms/__init__.py | 38 - .../crypto/_internal/algorithms/aes_cbc.py | 145 -- .../_internal/algorithms/aes_cbc_hmac.py | 149 -- .../crypto/_internal/algorithms/aes_kw.py | 68 - .../keys/crypto/_internal/algorithms/ecdsa.py | 60 - .../_internal/algorithms/rsa_encryption.py | 79 - .../_internal/algorithms/rsa_signing.py | 75 - .../keys/crypto/_internal/algorithms/sha_2.py | 53 - .../keyvault/keys/crypto/_internal/ec_key.py | 106 - .../keyvault/keys/crypto/_internal/key.py | 94 - .../keyvault/keys/crypto/_internal/rsa_key.py | 228 -- .../keys/crypto/_internal/symmetric_key.py | 125 - .../keys/crypto/_internal/transform.py | 61 - .../keyvault/keys/crypto/_key_validity.py | 16 - .../azure/keyvault/keys/crypto/_models.py | 621 ----- .../keys/crypto/_providers/__init__.py | 36 - .../keyvault/keys/crypto/_providers/ec.py | 34 - .../keys/crypto/_providers/local_provider.py | 104 - .../keyvault/keys/crypto/_providers/rsa.py | 34 - .../keys/crypto/_providers/symmetric.py | 28 - .../keyvault/keys/crypto/aio/__init__.py | 50 - .../azure/keyvault/keys/crypto/aio/_client.py | 503 ---- .../azure/keyvault/keys/py.typed | 0 .../azure-keyvault-keys/pyproject.toml | 46 +- .../azure-keyvault-keys/sdk_packaging.toml | 2 - .../azure-keyvault-keys/tests/_test_case.py | 1 - .../tests/perfstress_tests/decrypt.py | 1 - .../tests/perfstress_tests/sign.py | 1 - .../tests/perfstress_tests/unwrap.py | 1 - .../tests/test_challenge_auth.py | 1 + .../tests/test_challenge_auth_async.py | 1 + .../tests/test_crypto_client.py | 1 - .../tests/test_crypto_client_async.py | 1 - .../tests/test_keys_async.py | 1 - .../azure-keyvault-keys/tsp-location.yaml | 4 +- 83 files changed, 1314 insertions(+), 11281 deletions(-) delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_model_base.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_serialization.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_vendor.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_vendor.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py delete mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/py.typed delete mode 100644 sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml diff --git a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md index 8770a516b2b3..f912b86e1c50 100644 --- a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## 4.12.0b1 (2026-05-20) + +skip changelog generation for data-plane package and please add changelog manually. + ## 4.11.1 (2026-05-18) ### Features Added diff --git a/sdk/keyvault/azure-keyvault-keys/MANIFEST.in b/sdk/keyvault/azure-keyvault-keys/MANIFEST.in index 9bd7188bf0b2..7696bd6b2f38 100644 --- a/sdk/keyvault/azure-keyvault-keys/MANIFEST.in +++ b/sdk/keyvault/azure-keyvault-keys/MANIFEST.in @@ -1,6 +1,6 @@ include *.md include LICENSE -include azure/keyvault/keys/py.typed +include azure/keyvault/keys/_generated/py.typed recursive-include tests *.py recursive-include samples *.py *.md include azure/__init__.py diff --git a/sdk/keyvault/azure-keyvault-keys/_metadata.json b/sdk/keyvault/azure-keyvault-keys/_metadata.json index 0a2924fbf51d..f5e636422b96 100644 --- a/sdk/keyvault/azure-keyvault-keys/_metadata.json +++ b/sdk/keyvault/azure-keyvault-keys/_metadata.json @@ -1,6 +1,10 @@ { - "apiVersion": "2025-07-01", + "apiVersion": "2026-03-01-preview", "apiVersions": { - "KeyVault": "2025-07-01" - } + "KeyVault": "2026-03-01-preview" + }, + "commit": "9dd262c196cd5d27bb01a6bf4d645d9c89cc0f02", + "repository_url": "https://github.com/Azure/azure-rest-api-specs", + "typespec_src": "specification/keyvault/data-plane/Keys", + "emitterVersion": "0.62.1" } \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-keys/apiview-properties.json b/sdk/keyvault/azure-keyvault-keys/apiview-properties.json index 2fbacff777a3..31824d7af091 100644 --- a/sdk/keyvault/azure-keyvault-keys/apiview-properties.json +++ b/sdk/keyvault/azure-keyvault-keys/apiview-properties.json @@ -4,6 +4,7 @@ "azure.keyvault.keys._generated.models.BackupKeyResult": "KeyVault.BackupKeyResult", "azure.keyvault.keys._generated.models.DeletedKeyBundle": "KeyVault.DeletedKeyBundle", "azure.keyvault.keys._generated.models.DeletedKeyItem": "KeyVault.DeletedKeyItem", + "azure.keyvault.keys._generated.models.ExternalKey": "KeyVault.ExternalKey", "azure.keyvault.keys._generated.models.GetRandomBytesRequest": "KeyVault.GetRandomBytesRequest", "azure.keyvault.keys._generated.models.JsonWebKey": "KeyVault.JsonWebKey", "azure.keyvault.keys._generated.models.KeyAttestation": "KeyVault.KeyAttestation", @@ -30,12 +31,16 @@ "azure.keyvault.keys._generated.models.LifetimeActionsTrigger": "KeyVault.LifetimeActionsTrigger", "azure.keyvault.keys._generated.models.LifetimeActionsType": "KeyVault.LifetimeActionsType", "azure.keyvault.keys._generated.models.RandomBytes": "KeyVault.RandomBytes", + "azure.keyvault.keys._generated.models.SecureKeyOperationResult": "KeyVault.SecureKeyOperationResult", + "azure.keyvault.keys._generated.models.SecureKeyUnWrapOperationParameters": "KeyVault.SecureKeyUnWrapOperationParameters", + "azure.keyvault.keys._generated.models.SecureKeyWrapOperationParameters": "KeyVault.SecureKeyWrapOperationParameters", "azure.keyvault.keys._generated.models.JsonWebKeyType": "KeyVault.JsonWebKeyType", "azure.keyvault.keys._generated.models.JsonWebKeyCurveName": "KeyVault.JsonWebKeyCurveName", "azure.keyvault.keys._generated.models.DeletionRecoveryLevel": "KeyVault.DeletionRecoveryLevel", "azure.keyvault.keys._generated.models.JsonWebKeyOperation": "KeyVault.JsonWebKeyOperation", "azure.keyvault.keys._generated.models.JsonWebKeyEncryptionAlgorithm": "KeyVault.JsonWebKeyEncryptionAlgorithm", "azure.keyvault.keys._generated.models.JsonWebKeySignatureAlgorithm": "KeyVault.JsonWebKeySignatureAlgorithm", + "azure.keyvault.keys._generated.models.JsonWebKeyWrapAlgorithm": "KeyVault.JsonWebKeyWrapAlgorithm", "azure.keyvault.keys._generated.models.KeyEncryptionAlgorithm": "KeyVault.KeyEncryptionAlgorithm", "azure.keyvault.keys._generated.models.KeyRotationPolicyAction": "KeyVault.KeyRotationPolicyAction", "azure.keyvault.keys._generated.KeyVaultClient.create_key": "KeyVault.createKey", @@ -68,6 +73,10 @@ "azure.keyvault.keys._generated.aio.KeyVaultClient.verify": "KeyVault.verify", "azure.keyvault.keys._generated.KeyVaultClient.wrap_key": "KeyVault.wrapKey", "azure.keyvault.keys._generated.aio.KeyVaultClient.wrap_key": "KeyVault.wrapKey", + "azure.keyvault.keys._generated.KeyVaultClient.secure_wrap_key": "KeyVault.secureWrapKey", + "azure.keyvault.keys._generated.aio.KeyVaultClient.secure_wrap_key": "KeyVault.secureWrapKey", + "azure.keyvault.keys._generated.KeyVaultClient.secure_unwrap_key": "KeyVault.secureUnwrapKey", + "azure.keyvault.keys._generated.aio.KeyVaultClient.secure_unwrap_key": "KeyVault.secureUnwrapKey", "azure.keyvault.keys._generated.KeyVaultClient.unwrap_key": "KeyVault.unwrapKey", "azure.keyvault.keys._generated.aio.KeyVaultClient.unwrap_key": "KeyVault.unwrapKey", "azure.keyvault.keys._generated.KeyVaultClient.release": "KeyVault.release", @@ -88,5 +97,6 @@ "azure.keyvault.keys._generated.aio.KeyVaultClient.get_random_bytes": "KeyVault.getRandomBytes", "azure.keyvault.keys._generated.KeyVaultClient.get_key_attestation": "KeyVault.getKeyAttestation", "azure.keyvault.keys._generated.aio.KeyVaultClient.get_key_attestation": "KeyVault.getKeyAttestation" - } + }, + "CrossLanguageVersion": "8968be7e39f1" } \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py index 3a06bca6b656..d55ccad1f573 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py @@ -1,43 +1 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------- -from ._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation, KeyRotationPolicyAction, KeyType -from ._shared.client_base import ApiVersion -from ._models import ( - DeletedKey, - JsonWebKey, - KeyAttestation, - KeyProperties, - KeyReleasePolicy, - KeyRotationLifetimeAction, - KeyRotationPolicy, - KeyVaultKey, - KeyVaultKeyIdentifier, - ReleaseKeyResult, -) -from ._client import KeyClient - -__all__ = [ - "ApiVersion", - "KeyClient", - "JsonWebKey", - "KeyAttestation", - "KeyVaultKey", - "KeyVaultKeyIdentifier", - "KeyCurveName", - "KeyExportEncryptionAlgorithm", - "KeyOperation", - "KeyRotationPolicyAction", - "KeyType", - "DeletedKey", - "KeyProperties", - "KeyReleasePolicy", - "KeyRotationLifetimeAction", - "KeyRotationPolicy", - "ReleaseKeyResult", -] - -from ._version import VERSION - -__version__ = VERSION +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py deleted file mode 100644 index b1c2eb3a4b93..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py +++ /dev/null @@ -1,1010 +0,0 @@ -# pylint: disable=too-many-lines -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from datetime import datetime -from functools import partial -from typing import Any, Dict, List, Optional, Union - -from azure.core.paging import ItemPaged -from azure.core.polling import LROPoller -from azure.core.tracing.decorator import distributed_trace - -from .crypto import CryptographyClient -from ._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation, KeyType -from ._generated.models import KeyAttributes -from ._models import JsonWebKey, KeyRotationLifetimeAction -from ._shared import KeyVaultClientBase -from ._shared._polling import DeleteRecoverPollingMethod, KeyVaultOperationPoller -from ._models import DeletedKey, KeyVaultKey, KeyProperties, KeyReleasePolicy, KeyRotationPolicy, ReleaseKeyResult - - -def _get_key_id(vault_url, key_name, version=None): - without_version = f"{vault_url}/keys/{key_name}" - return without_version + "/" + version if version else without_version - - -class KeyClient(KeyVaultClientBase): - """A high-level interface for managing a vault's keys. - - :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". - You should validate that this URL references a valid Key Vault or Managed HSM resource. - See https://aka.ms/azsdk/blog/vault-uri for details. - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity` - :type credential: ~azure.core.credentials.TokenCredential - - :keyword api_version: Version of the service API to use. Defaults to the most recent. - :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str - :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key - Vault or Managed HSM domain. Defaults to True. - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START create_key_client] - :end-before: [END create_key_client] - :language: python - :caption: Create a new ``KeyClient`` - :dedent: 4 - """ - - # pylint:disable=protected-access, too-many-public-methods - - def _get_attributes( - self, - enabled: Optional[bool], - not_before: Optional[datetime], - expires_on: Optional[datetime], - exportable: Optional[bool] = None, - ) -> Optional[KeyAttributes]: - """Return a KeyAttributes object if non-None attributes are provided, or None otherwise. - - :param enabled: Whether the key is enabled. - :type enabled: bool or None - :param not_before: Not before date of the key in UTC. - :type not_before: ~datetime.datetime or None - :param expires_on: Expiry date of the key in UTC. - :type expires_on: ~datetime.datetime or None - :param exportable: Whether the private key can be exported. - :type exportable: bool or None - - :returns: An autorest-generated model of the key's attributes. - :rtype: KeyAttributes - """ - if enabled is not None or not_before is not None or expires_on is not None or exportable is not None: - return self._models.KeyAttributes( - enabled=enabled, not_before=not_before, expires=expires_on, exportable=exportable - ) - return None - - def get_cryptography_client( - self, - key_name: str, - *, - key_version: Optional[str] = None, - **kwargs, # pylint: disable=unused-argument - ) -> CryptographyClient: - """Gets a :class:`~azure.keyvault.keys.crypto.CryptographyClient` for the given key. - - :param str key_name: The name of the key used to perform cryptographic operations. - - :keyword key_version: Optional version of the key used to perform cryptographic operations. - :paramtype key_version: str or None - - :returns: A :class:`~azure.keyvault.keys.crypto.CryptographyClient` using the same options, credentials, and - HTTP client as this :class:`~azure.keyvault.keys.KeyClient`. - :rtype: ~azure.keyvault.keys.crypto.CryptographyClient - """ - key_id = _get_key_id(self._vault_url, key_name, key_version) - - # We provide a fake credential because the generated client already has the KeyClient's real credential - return CryptographyClient( - key_id, object(), generated_client=self._client, generated_models=self._models # type: ignore - ) - - @distributed_trace - def create_key( - self, - name: str, - key_type: Union[str, KeyType], - *, - size: Optional[int] = None, - curve: Optional[Union[str, KeyCurveName]] = None, - public_exponent: Optional[int] = None, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a key or, if ``name`` is already in use, create a new version of the key. - - Requires keys/create permission. - - :param str name: The name of the new key. - :param key_type: The type of key to create - :type key_type: ~azure.keyvault.keys.KeyType or str - - :keyword size: Key size in bits. Applies only to RSA and symmetric keys. Consider using - :func:`create_rsa_key` or :func:`create_oct_key` instead. - :paramtype size: int or None - :keyword curve: Elliptic curve name. Applies only to elliptic curve keys. Defaults to the NIST P-256 - elliptic curve. To create an elliptic curve key, consider using :func:`create_ec_key` instead. - :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None - :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. - :paramtype public_exponent: int or None - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START create_key] - :end-before: [END create_key] - :language: python - :caption: Create a key - :dedent: 8 - """ - attributes = self._get_attributes( - enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable - ) - - policy = release_policy - if policy is not None: - policy = self._models.KeyReleasePolicy( - encoded_policy=policy.encoded_policy, content_type=policy.content_type, immutable=policy.immutable - ) - parameters = self._models.KeyCreateParameters( - kty=key_type, - key_size=size, - key_attributes=attributes, - key_ops=key_operations, - tags=tags, - curve=curve, - public_exponent=public_exponent, - release_policy=policy, - ) - - bundle = self._client.create_key(key_name=name, parameters=parameters, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace - def create_rsa_key( - self, - name: str, - *, - size: Optional[int] = None, - public_exponent: Optional[int] = None, - hardware_protected: Optional[bool] = False, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a new RSA key or, if ``name`` is already in use, create a new version of the key - - Requires the keys/create permission. - - :param str name: The name for the new key. - - :keyword size: Key size in bits, for example 2048, 3072, or 4096. - :paramtype size: int or None - :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. - :paramtype public_exponent: int or None - :keyword hardware_protected: Whether the key should be created in a hardware security module. - Defaults to ``False``. - :paramtype hardware_protected: bool or None - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START create_rsa_key] - :end-before: [END create_rsa_key] - :language: python - :caption: Create RSA key - :dedent: 8 - """ - return self.create_key( - name, - key_type="RSA-HSM" if hardware_protected else "RSA", - size=size, - public_exponent=public_exponent, - key_operations=key_operations, - enabled=enabled, - tags=tags, - not_before=not_before, - expires_on=expires_on, - exportable=exportable, - release_policy=release_policy, - **kwargs, - ) - - @distributed_trace - def create_ec_key( - self, - name: str, - *, - curve: Optional[Union[str, KeyCurveName]] = None, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - hardware_protected: Optional[bool] = False, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a new elliptic curve key or, if ``name`` is already in use, create a new version of the key. - - Requires the keys/create permission. - - :param str name: The name for the new key. - - :keyword curve: Elliptic curve name. Defaults to the NIST P-256 elliptic curve. - :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword hardware_protected: Whether the key should be created in a hardware security module. - Defaults to ``False``. - :paramtype hardware_protected: bool or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START create_ec_key] - :end-before: [END create_ec_key] - :language: python - :caption: Create an elliptic curve key - :dedent: 8 - """ - return self.create_key( - name, - key_type="EC-HSM" if hardware_protected else "EC", - curve=curve, - key_operations=key_operations, - enabled=enabled, - tags=tags, - not_before=not_before, - expires_on=expires_on, - exportable=exportable, - release_policy=release_policy, - **kwargs, - ) - - @distributed_trace - def create_oct_key( - self, - name: str, - *, - size: Optional[int] = None, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - hardware_protected: Optional[bool] = False, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a new octet sequence (symmetric) key or, if ``name`` is in use, create a new version of the key. - - Requires the keys/create permission. - - :param str name: The name for the new key. - - :keyword size: Key size in bits, for example 128, 192, or 256. - :paramtype size: int or None - :keyword key_operations: Allowed key operations. - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword hardware_protected: Whether the key should be created in a hardware security module. - Defaults to ``False``. - :paramtype hardware_protected: bool or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START create_oct_key] - :end-before: [END create_oct_key] - :language: python - :caption: Create an octet sequence (symmetric) key - :dedent: 8 - """ - return self.create_key( - name, - key_type="oct-HSM" if hardware_protected else "oct", - size=size, - key_operations=key_operations, - enabled=enabled, - tags=tags, - not_before=not_before, - expires_on=expires_on, - exportable=exportable, - release_policy=release_policy, - **kwargs, - ) - - @distributed_trace - def begin_delete_key( # pylint:disable=bad-option-value,delete-operation-wrong-return-type - self, name: str, **kwargs: Any - ) -> LROPoller[DeletedKey]: - """Delete all versions of a key and its cryptographic material. - - Requires keys/delete permission. When this method returns Key Vault has begun deleting the key. Deletion may - take several seconds in a vault with soft-delete enabled. This method therefore returns a poller enabling you to - wait for deletion to complete. - - :param str name: The name of the key to delete. - - :returns: A poller for the delete key operation. The poller's `result` method returns the - :class:`~azure.keyvault.keys.DeletedKey` without waiting for deletion to complete. If the vault has - soft-delete enabled and you want to permanently delete the key with :func:`purge_deleted_key`, call the - poller's `wait` method first. It will block until the deletion is complete. The `wait` method requires - keys/get permission. - :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.keys.DeletedKey] - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START delete_key] - :end-before: [END delete_key] - :language: python - :caption: Delete a key - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, deleted_key_bundle = self._client.delete_key( - key_name=name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - deleted_key = DeletedKey._from_deleted_key_bundle(deleted_key_bundle) - - command = partial(self.get_deleted_key, name=name, **kwargs) - polling_method = DeleteRecoverPollingMethod( - # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished - finished=deleted_key.recovery_id is None, - pipeline_response=pipeline_response, - command=command, - final_resource=deleted_key, - interval=polling_interval, - ) - return KeyVaultOperationPoller(polling_method) - - @distributed_trace - def get_key(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: - """Get a key's attributes and, if it's an asymmetric key, its public material. - - Requires keys/get permission. - - :param str name: The name of the key to get. - :param version: (optional) A specific version of the key to get. If not specified, gets the latest version - of the key. - :type version: str or None - - :returns: The fetched key. - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START get_key] - :end-before: [END get_key] - :language: python - :caption: Get a key - :dedent: 8 - """ - bundle = self._client.get_key(name, key_version=version or "", **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace - def get_deleted_key(self, name: str, **kwargs: Any) -> DeletedKey: - """Get a deleted key. Possible only in a vault with soft-delete enabled. - - Requires keys/get permission. - - :param str name: The name of the key - - :returns: The deleted key - :rtype: ~azure.keyvault.keys.DeletedKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START get_deleted_key] - :end-before: [END get_deleted_key] - :language: python - :caption: Get a deleted key - :dedent: 8 - """ - bundle = self._client.get_deleted_key(name, **kwargs) - return DeletedKey._from_deleted_key_bundle(bundle) - - @distributed_trace - def list_deleted_keys(self, **kwargs: Any) -> ItemPaged[DeletedKey]: - """List all deleted keys, including the public part of each. Possible only in a vault with soft-delete enabled. - - Requires keys/list permission. - - :returns: An iterator of deleted keys - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.keys.DeletedKey] - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START list_deleted_keys] - :end-before: [END list_deleted_keys] - :language: python - :caption: List all the deleted keys - :dedent: 8 - """ - return self._client.get_deleted_keys( - maxresults=kwargs.pop("max_page_size", None), - cls=lambda objs: [DeletedKey._from_deleted_key_item(x) for x in objs], - **kwargs, - ) - - @distributed_trace - def list_properties_of_keys(self, **kwargs: Any) -> ItemPaged[KeyProperties]: - """List identifiers and properties of all keys in the vault. - - Requires keys/list permission. - - :returns: An iterator of keys without their cryptographic material or version information - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.keys.KeyProperties] - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START list_keys] - :end-before: [END list_keys] - :language: python - :caption: List all keys - :dedent: 8 - """ - return self._client.get_keys( - maxresults=kwargs.pop("max_page_size", None), - cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], - **kwargs, - ) - - @distributed_trace - def list_properties_of_key_versions(self, name: str, **kwargs: Any) -> ItemPaged[KeyProperties]: - """List the identifiers and properties of a key's versions. - - Requires keys/list permission. - - :param str name: The name of the key - - :returns: An iterator of keys without their cryptographic material - :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.keys.KeyProperties] - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START list_properties_of_key_versions] - :end-before: [END list_properties_of_key_versions] - :language: python - :caption: List all versions of a key - :dedent: 8 - """ - return self._client.get_key_versions( - name, - maxresults=kwargs.pop("max_page_size", None), - cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], - **kwargs, - ) - - @distributed_trace - def purge_deleted_key(self, name: str, **kwargs: Any) -> None: - """Permanently deletes a deleted key. Only possible in a vault with soft-delete enabled. - - Performs an irreversible deletion of the specified key, without possibility for recovery. The operation is not - available if the :py:attr:`~azure.keyvault.keys.KeyProperties.recovery_level` does not specify 'Purgeable'. - This method is only necessary for purging a key before its - :py:attr:`~azure.keyvault.keys.DeletedKey.scheduled_purge_date`. - - Requires keys/purge permission. - - :param str name: The name of the deleted key to purge - - :returns: None - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. code-block:: python - - # if the vault has soft-delete enabled, purge permanently deletes a deleted key - # (with soft-delete disabled, begin_delete_key is permanent) - key_client.purge_deleted_key("key-name") - - """ - self._client.purge_deleted_key(key_name=name, **kwargs) - - @distributed_trace - def begin_recover_deleted_key(self, name: str, **kwargs: Any) -> LROPoller[KeyVaultKey]: - """Recover a deleted key to its latest version. Possible only in a vault with soft-delete enabled. - - Requires keys/recover permission. - - When this method returns Key Vault has begun recovering the key. Recovery may take several seconds. This - method therefore returns a poller enabling you to wait for recovery to complete. Waiting is only necessary when - you want to use the recovered key in another operation immediately. - - :param str name: The name of the deleted key to recover - - :returns: A poller for the recovery operation. The poller's `result` method returns the recovered - :class:`~azure.keyvault.keys.KeyVaultKey` without waiting for recovery to complete. If you want to use the - recovered key immediately, call the poller's `wait` method, which blocks until the key is ready to use. The - `wait` method requires keys/get permission. - :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.keys.KeyVaultKey] - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START recover_deleted_key] - :end-before: [END recover_deleted_key] - :language: python - :caption: Recover a deleted key - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, recovered_key_bundle = self._client.recover_deleted_key( - key_name=name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - recovered_key = KeyVaultKey._from_key_bundle(recovered_key_bundle) - command = partial(self.get_key, name=name, **kwargs) - polling_method = DeleteRecoverPollingMethod( - finished=False, - pipeline_response=pipeline_response, - command=command, - final_resource=recovered_key, - interval=polling_interval, - ) - - return KeyVaultOperationPoller(polling_method) - - @distributed_trace - def update_key_properties( - self, - name: str, - version: Optional[str] = None, - *, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Change a key's properties (not its cryptographic material). - - Requires keys/update permission. - - :param str name: The name of key to update - :param version: (optional) The version of the key to update. If unspecified, the latest version is updated. - :type version: str or None - - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The updated key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START update_key] - :end-before: [END update_key] - :language: python - :caption: Update a key's attributes - :dedent: 8 - """ - attributes = self._get_attributes(enabled=enabled, not_before=not_before, expires_on=expires_on) - - policy = release_policy - if policy is not None: - policy = self._models.KeyReleasePolicy( - content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable - ) - parameters = self._models.KeyUpdateParameters( - key_ops=key_operations, - key_attributes=attributes, - tags=tags, - release_policy=policy, - ) - - bundle = self._client.update_key(name, key_version=version or "", parameters=parameters, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace - def backup_key(self, name: str, **kwargs: Any) -> bytes: - """Back up a key in a protected form useable only by Azure Key Vault. - - Requires keys/backup permission. - - This is intended to allow copying a key from one vault to another. Both vaults must be owned by the same Azure - subscription. Also, backup / restore cannot be performed across geopolitical boundaries. For example, a backup - from a vault in a USA region cannot be restored to a vault in an EU region. - - :param str name: The name of the key to back up - - :returns: The key backup result, in a protected bytes format that can only be used by Azure Key Vault. - :rtype: bytes - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START backup_key] - :end-before: [END backup_key] - :language: python - :caption: Get a key backup - :dedent: 8 - """ - backup_result = self._client.backup_key(name, **kwargs) - return backup_result.value - - @distributed_trace - def restore_key_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultKey: - """Restore a key backup to the vault. - - Requires keys/restore permission. - - This imports all versions of the key, with its name, attributes, and access control policies. If the key's name - is already in use, restoring it will fail. Also, the target vault must be owned by the same Microsoft Azure - subscription as the source vault. - - :param bytes backup: A key backup as returned by :func:`backup_key` - - :returns: The restored key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.ResourceExistsError or ~azure.core.exceptions.HttpResponseError: - the former if the backed up key's name is already in use; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys.py - :start-after: [START restore_key_backup] - :end-before: [END restore_key_backup] - :language: python - :caption: Restore a key backup - :dedent: 8 - """ - bundle = self._client.restore_key( - parameters=self._models.KeyRestoreParameters(key_bundle_backup=backup), **kwargs - ) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace - def import_key( - self, - name: str, - key: JsonWebKey, - *, - hardware_protected: Optional[bool] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Import a key created externally. - - Requires keys/import permission. If ``name`` is already in use, the key will be imported as a new version. - - :param str name: Name for the imported key - :param key: The JSON web key to import - :type key: ~azure.keyvault.keys.JsonWebKey - - :keyword hardware_protected: Whether the key should be backed by a hardware security module - :paramtype hardware_protected: bool or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The imported key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - """ - attributes = self._get_attributes( - enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable - ) - - policy = release_policy - if policy is not None: - policy = self._models.KeyReleasePolicy( - content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable - ) - parameters = self._models.KeyImportParameters( - key=key._to_generated_model(), - key_attributes=attributes, - hsm=hardware_protected, - tags=tags, - release_policy=policy, - ) - - bundle = self._client.import_key(name, parameters=parameters, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace - def release_key( - self, - name: str, - target_attestation_token: str, - *, - version: Optional[str] = None, - algorithm: Optional[Union[str, KeyExportEncryptionAlgorithm]] = None, - nonce: Optional[str] = None, - **kwargs: Any, - ) -> ReleaseKeyResult: - """Releases a key. - - The release key operation is applicable to all key types. The target key must be marked - exportable. This operation requires the keys/release permission. - - :param str name: The name of the key to get. - :param str target_attestation_token: The attestation assertion for the target of the key release. - - :keyword version: A specific version of the key to release. If unspecified, the latest version is released. - :paramtype version: str or None - :keyword algorithm: The encryption algorithm to use to protect the released key material. - :paramtype algorithm: str or ~azure.keyvault.keys.KeyExportEncryptionAlgorithm or None - :keyword nonce: A client-provided nonce for freshness. - :paramtype nonce: str or None - - :return: The result of the key release. - :rtype: ~azure.keyvault.keys.ReleaseKeyResult - - :raises ~azure.core.exceptions.HttpResponseError: - """ - result = self._client.release( - key_name=name, - key_version=version or "", - parameters=self._models.KeyReleaseParameters( - target_attestation_token=target_attestation_token, - nonce=nonce, - enc=algorithm, - ), - **kwargs, - ) - return ReleaseKeyResult(result.value) - - @distributed_trace - def get_random_bytes(self, count: int, **kwargs: Any) -> bytes: - """Get the requested number of random bytes from a managed HSM. - - :param int count: The requested number of random bytes. - - :return: The random bytes. - :rtype: bytes - - :raises ValueError or ~azure.core.exceptions.HttpResponseError: - the former if less than one random byte is requested; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_key_client.py - :start-after: [START get_random_bytes] - :end-before: [END get_random_bytes] - :language: python - :caption: Get random bytes - :dedent: 12 - """ - if count < 1: - raise ValueError("At least one random byte must be requested") - parameters = self._models.GetRandomBytesRequest(count=count) - result = self._client.get_random_bytes(parameters=parameters, **kwargs) - return result.value - - @distributed_trace - def get_key_rotation_policy(self, key_name: str, **kwargs: Any) -> KeyRotationPolicy: - """Get the rotation policy of a Key Vault key. - - :param str key_name: The name of the key. - - :return: The key rotation policy. - :rtype: ~azure.keyvault.keys.KeyRotationPolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - policy = self._client.get_key_rotation_policy(key_name=key_name, **kwargs) - return KeyRotationPolicy._from_generated(policy) - - @distributed_trace - def rotate_key(self, name: str, **kwargs: Any) -> KeyVaultKey: - """Rotate the key based on the key policy by generating a new version of the key. - - This operation requires the keys/rotate permission. - - :param str name: The name of the key to rotate. - - :return: The new version of the rotated key. - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = self._client.rotate_key(key_name=name, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace - def update_key_rotation_policy( # pylint: disable=unused-argument - self, - key_name: str, - policy: KeyRotationPolicy, - *, - lifetime_actions: Optional[List[KeyRotationLifetimeAction]] = None, - expires_in: Optional[str] = None, - **kwargs: Any, - ) -> KeyRotationPolicy: - """Updates the rotation policy of a Key Vault key. - - This operation requires the keys/update permission. - - :param str key_name: The name of the key in the given vault. - :param policy: The new rotation policy for the key. - :type policy: ~azure.keyvault.keys.KeyRotationPolicy - - :keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key. This will - override the lifetime actions of the provided ``policy``. - :paramtype lifetime_actions: List[~azure.keyvault.keys.KeyRotationLifetimeAction] - :keyword str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an - ISO 8601 duration. For example: 90 days is "P90D", 3 months is "P3M", and 48 hours is "PT48H". See - `Wikipedia `_ for more information on ISO 8601 durations. - This will override the expiry time of the provided ``policy``. - - :return: The updated rotation policy. - :rtype: ~azure.keyvault.keys.KeyRotationPolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - actions = lifetime_actions or policy.lifetime_actions - if actions: - actions = [ - self._models.LifetimeActions( - action=self._models.LifetimeActionsType(type=action.action), - trigger=self._models.LifetimeActionsTrigger( - time_after_create=action.time_after_create, time_before_expiry=action.time_before_expiry - ), - ) - for action in actions - ] - - attributes = self._models.KeyRotationPolicyAttributes(expiry_time=expires_in or policy.expires_in) - new_policy = self._models.KeyRotationPolicy(lifetime_actions=actions or [], attributes=attributes) - result = self._client.update_key_rotation_policy(key_name=key_name, key_rotation_policy=new_policy) - return KeyRotationPolicy._from_generated(result) - - @distributed_trace - def get_key_attestation(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: - """Get a key and its attestation blob. - - This method is applicable to any key stored in Azure Key Vault Managed HSM. This operation requires the keys/get - permission. - - :param str name: The name of the key. - :param version: (optional) A specific version of the key to get. If not specified, gets the latest version - of the key. - :type version: str or None - - :return: The key attestation. - :rtype: ~azure.keyvault.keys.KeyAttestation - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = self._client.get_key_attestation(key_name=name, key_version=version or "", **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - def __enter__(self) -> "KeyClient": - self._client.__enter__() - return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py deleted file mode 100644 index 24dab8ff5ad7..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py +++ /dev/null @@ -1,72 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -# pylint: disable=enum-must-be-uppercase - -from enum import Enum - -from azure.core import CaseInsensitiveEnumMeta - - -class KeyCurveName(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Supported elliptic curves""" - - p_256 = "P-256" #: The NIST P-256 elliptic curve, AKA SECG curve SECP256R1. - p_384 = "P-384" #: The NIST P-384 elliptic curve, AKA SECG curve SECP384R1. - p_521 = "P-521" #: The NIST P-521 elliptic curve, AKA SECG curve SECP521R1. - p_256_k = "P-256K" #: The SECG SECP256K1 elliptic curve. - - -class KeyExportEncryptionAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Supported algorithms for protecting exported key material""" - - ckm_rsa_aes_key_wrap = "CKM_RSA_AES_KEY_WRAP" - rsa_aes_key_wrap_256 = "RSA_AES_KEY_WRAP_256" - rsa_aes_key_wrap_384 = "RSA_AES_KEY_WRAP_384" - - -class KeyOperation(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Supported key operations""" - - encrypt = "encrypt" - decrypt = "decrypt" - import_key = "import" - sign = "sign" - verify = "verify" - wrap_key = "wrapKey" - unwrap_key = "unwrapKey" - export = "export" - - -class KeyRotationPolicyAction(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """The action that will be executed in a key rotation policy""" - - rotate = "Rotate" #: Rotate the key based on the key policy. - notify = "Notify" #: Trigger Event Grid events. - - @classmethod - def _missing_(cls, value): - for member in cls: - if member.value.lower() == value.lower(): - return member - raise ValueError(f"{value} is not a valid KeyRotationPolicyAction") - - -class KeyType(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Supported key types""" - - ec = "EC" #: Elliptic Curve - ec_hsm = "EC-HSM" #: Elliptic Curve with a private key which is not exportable from the HSM - rsa = "RSA" #: RSA (https://tools.ietf.org/html/rfc3447) - rsa_hsm = "RSA-HSM" #: RSA with a private key which is not exportable from the HSM - oct = "oct" #: Octet sequence (used to represent symmetric keys) - oct_hsm = "oct-HSM" #: Octet sequence with a private key which is not exportable from the HSM - - @classmethod - def _missing_(cls, value): - for member in cls: - if member.value.lower() == value.lower(): - return member - raise ValueError(f"{value} is not a valid KeyType") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_client.py index 0ab087a35122..67aabf6451b6 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_client.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_client.py @@ -7,8 +7,8 @@ # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, TYPE_CHECKING -from typing_extensions import Self from azure.core import PipelineClient from azure.core.pipeline import policies @@ -18,6 +18,11 @@ from ._operations import _KeyVaultClientOperationsMixin from ._utils.serialization import Deserializer, Serializer +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore + if TYPE_CHECKING: from azure.core.credentials import TokenCredential @@ -30,9 +35,10 @@ class KeyVaultClient(_KeyVaultClientOperationsMixin): :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_configuration.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_configuration.py index ea1708489bee..dd8c552cd8b1 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_configuration.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_configuration.py @@ -26,14 +26,15 @@ class KeyVaultClientConfiguration: # pylint: disable=too-many-instance-attribut :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials.TokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ def __init__(self, vault_base_url: str, credential: "TokenCredential", **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2025-07-01") + api_version: str = kwargs.pop("api_version", "2026-03-01-preview") if vault_base_url is None: raise ValueError("Parameter 'vault_base_url' must not be None.") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_model_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_model_base.py deleted file mode 100644 index 3072ee252ed9..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_model_base.py +++ /dev/null @@ -1,1235 +0,0 @@ -# pylint: disable=too-many-lines -# coding=utf-8 -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -# pylint: disable=protected-access, broad-except - -import copy -import calendar -import decimal -import functools -import sys -import logging -import base64 -import re -import typing -import enum -import email.utils -from datetime import datetime, date, time, timedelta, timezone -from json import JSONEncoder -import xml.etree.ElementTree as ET -from typing_extensions import Self -import isodate -from azure.core.exceptions import DeserializationError -from azure.core import CaseInsensitiveEnumMeta -from azure.core.pipeline import PipelineResponse -from azure.core.serialization import _Null - -if sys.version_info >= (3, 9): - from collections.abc import MutableMapping -else: - from typing import MutableMapping - -_LOGGER = logging.getLogger(__name__) - -__all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] - -TZ_UTC = timezone.utc -_T = typing.TypeVar("_T") - - -def _timedelta_as_isostr(td: timedelta) -> str: - """Converts a datetime.timedelta object into an ISO 8601 formatted string, e.g. 'P4DT12H30M05S' - - Function adapted from the Tin Can Python project: https://github.com/RusticiSoftware/TinCanPython - - :param timedelta td: The timedelta to convert - :rtype: str - :return: ISO8601 version of this timedelta - """ - - # Split seconds to larger units - seconds = td.total_seconds() - minutes, seconds = divmod(seconds, 60) - hours, minutes = divmod(minutes, 60) - days, hours = divmod(hours, 24) - - days, hours, minutes = list(map(int, (days, hours, minutes))) - seconds = round(seconds, 6) - - # Build date - date_str = "" - if days: - date_str = "%sD" % days - - if hours or minutes or seconds: - # Build time - time_str = "T" - - # Hours - bigger_exists = date_str or hours - if bigger_exists: - time_str += "{:02}H".format(hours) - - # Minutes - bigger_exists = bigger_exists or minutes - if bigger_exists: - time_str += "{:02}M".format(minutes) - - # Seconds - try: - if seconds.is_integer(): - seconds_string = "{:02}".format(int(seconds)) - else: - # 9 chars long w/ leading 0, 6 digits after decimal - seconds_string = "%09.6f" % seconds - # Remove trailing zeros - seconds_string = seconds_string.rstrip("0") - except AttributeError: # int.is_integer() raises - seconds_string = "{:02}".format(seconds) - - time_str += "{}S".format(seconds_string) - else: - time_str = "" - - return "P" + date_str + time_str - - -def _serialize_bytes(o, format: typing.Optional[str] = None) -> str: - encoded = base64.b64encode(o).decode() - if format == "base64url": - return encoded.strip("=").replace("+", "-").replace("/", "_") - return encoded - - -def _serialize_datetime(o, format: typing.Optional[str] = None): - if hasattr(o, "year") and hasattr(o, "hour"): - if format == "rfc7231": - return email.utils.format_datetime(o, usegmt=True) - if format == "unix-timestamp": - return int(calendar.timegm(o.utctimetuple())) - - # astimezone() fails for naive times in Python 2.7, so make make sure o is aware (tzinfo is set) - if not o.tzinfo: - iso_formatted = o.replace(tzinfo=TZ_UTC).isoformat() - else: - iso_formatted = o.astimezone(TZ_UTC).isoformat() - # Replace the trailing "+00:00" UTC offset with "Z" (RFC 3339: https://www.ietf.org/rfc/rfc3339.txt) - return iso_formatted.replace("+00:00", "Z") - # Next try datetime.date or datetime.time - return o.isoformat() - - -def _is_readonly(p): - try: - return p._visibility == ["read"] - except AttributeError: - return False - - -class SdkJSONEncoder(JSONEncoder): - """A JSON encoder that's capable of serializing datetime objects and bytes.""" - - def __init__(self, *args, exclude_readonly: bool = False, format: typing.Optional[str] = None, **kwargs): - super().__init__(*args, **kwargs) - self.exclude_readonly = exclude_readonly - self.format = format - - def default(self, o): # pylint: disable=too-many-return-statements - if _is_model(o): - if self.exclude_readonly: - readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] - return {k: v for k, v in o.items() if k not in readonly_props} - return dict(o.items()) - try: - return super(SdkJSONEncoder, self).default(o) - except TypeError: - if isinstance(o, _Null): - return None - if isinstance(o, decimal.Decimal): - return float(o) - if isinstance(o, (bytes, bytearray)): - return _serialize_bytes(o, self.format) - try: - # First try datetime.datetime - return _serialize_datetime(o, self.format) - except AttributeError: - pass - # Last, try datetime.timedelta - try: - return _timedelta_as_isostr(o) - except AttributeError: - # This will be raised when it hits value.total_seconds in the method above - pass - return super(SdkJSONEncoder, self).default(o) - - -_VALID_DATE = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}" + r"\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") -_VALID_RFC7231 = re.compile( - r"(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s\d{2}\s" - r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT" -) - - -def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime: - """Deserialize ISO-8601 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - attr = attr.upper() - match = _VALID_DATE.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - check_decimal = attr.split(".") - if len(check_decimal) > 1: - decimal_str = "" - for digit in check_decimal[1]: - if digit.isdigit(): - decimal_str += digit - else: - break - if len(decimal_str) > 6: - attr = attr.replace(decimal_str, decimal_str[0:6]) - - date_obj = isodate.parse_datetime(attr) - test_utc = date_obj.utctimetuple() - if test_utc.tm_year > 9999 or test_utc.tm_year < 1: - raise OverflowError("Hit max or min date") - return date_obj - - -def _deserialize_datetime_rfc7231(attr: typing.Union[str, datetime]) -> datetime: - """Deserialize RFC7231 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - match = _VALID_RFC7231.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - return email.utils.parsedate_to_datetime(attr) - - -def _deserialize_datetime_unix_timestamp(attr: typing.Union[float, datetime]) -> datetime: - """Deserialize unix timestamp into Datetime object. - - :param str attr: response string to be deserialized. - :rtype: ~datetime.datetime - :returns: The datetime object from that input - """ - if isinstance(attr, datetime): - # i'm already deserialized - return attr - return datetime.fromtimestamp(attr, TZ_UTC) - - -def _deserialize_date(attr: typing.Union[str, date]) -> date: - """Deserialize ISO-8601 formatted string into Date object. - :param str attr: response string to be deserialized. - :rtype: date - :returns: The date object from that input - """ - # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. - if isinstance(attr, date): - return attr - return isodate.parse_date(attr, defaultmonth=None, defaultday=None) # type: ignore - - -def _deserialize_time(attr: typing.Union[str, time]) -> time: - """Deserialize ISO-8601 formatted string into time object. - - :param str attr: response string to be deserialized. - :rtype: datetime.time - :returns: The time object from that input - """ - if isinstance(attr, time): - return attr - return isodate.parse_time(attr) - - -def _deserialize_bytes(attr): - if isinstance(attr, (bytes, bytearray)): - return attr - return bytes(base64.b64decode(attr)) - - -def _deserialize_bytes_base64(attr): - if isinstance(attr, (bytes, bytearray)): - return attr - padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore - attr = attr + padding # type: ignore - encoded = attr.replace("-", "+").replace("_", "/") - return bytes(base64.b64decode(encoded)) - - -def _deserialize_duration(attr): - if isinstance(attr, timedelta): - return attr - return isodate.parse_duration(attr) - - -def _deserialize_decimal(attr): - if isinstance(attr, decimal.Decimal): - return attr - return decimal.Decimal(str(attr)) - - -def _deserialize_int_as_str(attr): - if isinstance(attr, int): - return attr - return int(attr) - - -_DESERIALIZE_MAPPING = { - datetime: _deserialize_datetime, - date: _deserialize_date, - time: _deserialize_time, - bytes: _deserialize_bytes, - bytearray: _deserialize_bytes, - timedelta: _deserialize_duration, - typing.Any: lambda x: x, - decimal.Decimal: _deserialize_decimal, -} - -_DESERIALIZE_MAPPING_WITHFORMAT = { - "rfc3339": _deserialize_datetime, - "rfc7231": _deserialize_datetime_rfc7231, - "unix-timestamp": _deserialize_datetime_unix_timestamp, - "base64": _deserialize_bytes, - "base64url": _deserialize_bytes_base64, -} - - -def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None): - if annotation is int and rf and rf._format == "str": - return _deserialize_int_as_str - if rf and rf._format: - return _DESERIALIZE_MAPPING_WITHFORMAT.get(rf._format) - return _DESERIALIZE_MAPPING.get(annotation) # pyright: ignore - - -def _get_type_alias_type(module_name: str, alias_name: str): - types = { - k: v - for k, v in sys.modules[module_name].__dict__.items() - if isinstance(v, typing._GenericAlias) # type: ignore - } - if alias_name not in types: - return alias_name - return types[alias_name] - - -def _get_model(module_name: str, model_name: str): - models = {k: v for k, v in sys.modules[module_name].__dict__.items() if isinstance(v, type)} - module_end = module_name.rsplit(".", 1)[0] - models.update({k: v for k, v in sys.modules[module_end].__dict__.items() if isinstance(v, type)}) - if isinstance(model_name, str): - model_name = model_name.split(".")[-1] - if model_name not in models: - return model_name - return models[model_name] - - -_UNSET = object() - - -class _MyMutableMapping(MutableMapping[str, typing.Any]): # pylint: disable=unsubscriptable-object - def __init__(self, data: typing.Dict[str, typing.Any]) -> None: - self._data = data - - def __contains__(self, key: typing.Any) -> bool: - return key in self._data - - def __getitem__(self, key: str) -> typing.Any: - return self._data.__getitem__(key) - - def __setitem__(self, key: str, value: typing.Any) -> None: - self._data.__setitem__(key, value) - - def __delitem__(self, key: str) -> None: - self._data.__delitem__(key) - - def __iter__(self) -> typing.Iterator[typing.Any]: - return self._data.__iter__() - - def __len__(self) -> int: - return self._data.__len__() - - def __ne__(self, other: typing.Any) -> bool: - return not self.__eq__(other) - - def keys(self) -> typing.KeysView[str]: - """ - :returns: a set-like object providing a view on D's keys - :rtype: ~typing.KeysView - """ - return self._data.keys() - - def values(self) -> typing.ValuesView[typing.Any]: - """ - :returns: an object providing a view on D's values - :rtype: ~typing.ValuesView - """ - return self._data.values() - - def items(self) -> typing.ItemsView[str, typing.Any]: - """ - :returns: set-like object providing a view on D's items - :rtype: ~typing.ItemsView - """ - return self._data.items() - - def get(self, key: str, default: typing.Any = None) -> typing.Any: - """ - Get the value for key if key is in the dictionary, else default. - :param str key: The key to look up. - :param any default: The value to return if key is not in the dictionary. Defaults to None - :returns: D[k] if k in D, else d. - :rtype: any - """ - try: - return self[key] - except KeyError: - return default - - @typing.overload - def pop(self, key: str) -> typing.Any: ... - - @typing.overload - def pop(self, key: str, default: _T) -> _T: ... - - @typing.overload - def pop(self, key: str, default: typing.Any) -> typing.Any: ... - - def pop(self, key: str, default: typing.Any = _UNSET) -> typing.Any: - """ - Removes specified key and return the corresponding value. - :param str key: The key to pop. - :param any default: The value to return if key is not in the dictionary - :returns: The value corresponding to the key. - :rtype: any - :raises KeyError: If key is not found and default is not given. - """ - if default is _UNSET: - return self._data.pop(key) - return self._data.pop(key, default) - - def popitem(self) -> typing.Tuple[str, typing.Any]: - """ - Removes and returns some (key, value) pair - :returns: The (key, value) pair. - :rtype: tuple - :raises KeyError: if D is empty. - """ - return self._data.popitem() - - def clear(self) -> None: - """ - Remove all items from D. - """ - self._data.clear() - - def update(self, *args: typing.Any, **kwargs: typing.Any) -> None: - """ - Updates D from mapping/iterable E and F. - :param any args: Either a mapping object or an iterable of key-value pairs. - """ - self._data.update(*args, **kwargs) - - @typing.overload - def setdefault(self, key: str, default: None = None) -> None: ... - - @typing.overload - def setdefault(self, key: str, default: typing.Any) -> typing.Any: ... - - def setdefault(self, key: str, default: typing.Any = _UNSET) -> typing.Any: - """ - Same as calling D.get(k, d), and setting D[k]=d if k not found - :param str key: The key to look up. - :param any default: The value to set if key is not in the dictionary - :returns: D[k] if k in D, else d. - :rtype: any - """ - if default is _UNSET: - return self._data.setdefault(key) - return self._data.setdefault(key, default) - - def __eq__(self, other: typing.Any) -> bool: - try: - other_model = self.__class__(other) - except Exception: - return False - return self._data == other_model._data - - def __repr__(self) -> str: - return str(self._data) - - -def _is_model(obj: typing.Any) -> bool: - return getattr(obj, "_is_model", False) - - -def _serialize(o, format: typing.Optional[str] = None): # pylint: disable=too-many-return-statements - if isinstance(o, list): - return [_serialize(x, format) for x in o] - if isinstance(o, dict): - return {k: _serialize(v, format) for k, v in o.items()} - if isinstance(o, set): - return {_serialize(x, format) for x in o} - if isinstance(o, tuple): - return tuple(_serialize(x, format) for x in o) - if isinstance(o, (bytes, bytearray)): - return _serialize_bytes(o, format) - if isinstance(o, decimal.Decimal): - return float(o) - if isinstance(o, enum.Enum): - return o.value - if isinstance(o, int): - if format == "str": - return str(o) - return o - try: - # First try datetime.datetime - return _serialize_datetime(o, format) - except AttributeError: - pass - # Last, try datetime.timedelta - try: - return _timedelta_as_isostr(o) - except AttributeError: - # This will be raised when it hits value.total_seconds in the method above - pass - return o - - -def _get_rest_field( - attr_to_rest_field: typing.Dict[str, "_RestField"], rest_name: str -) -> typing.Optional["_RestField"]: - try: - return next(rf for rf in attr_to_rest_field.values() if rf._rest_name == rest_name) - except StopIteration: - return None - - -def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typing.Any: - if not rf: - return _serialize(value, None) - if rf._is_multipart_file_input: - return value - if rf._is_model: - return _deserialize(rf._type, value) - if isinstance(value, ET.Element): - value = _deserialize(rf._type, value) - return _serialize(value, rf._format) - - -class Model(_MyMutableMapping): - _is_model = True - # label whether current class's _attr_to_rest_field has been calculated - # could not see _attr_to_rest_field directly because subclass inherits it from parent class - _calculated: typing.Set[str] = set() - - def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: - class_name = self.__class__.__name__ - if len(args) > 1: - raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") - dict_to_pass = { - rest_field._rest_name: rest_field._default - for rest_field in self._attr_to_rest_field.values() - if rest_field._default is not _UNSET - } - if args: # pylint: disable=too-many-nested-blocks - if isinstance(args[0], ET.Element): - existed_attr_keys = [] - model_meta = getattr(self, "_xml", {}) - - for rf in self._attr_to_rest_field.values(): - prop_meta = getattr(rf, "_xml", {}) - xml_name = prop_meta.get("name", rf._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - # attribute - if prop_meta.get("attribute", False) and args[0].get(xml_name) is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].get(xml_name)) - continue - - # unwrapped element is array - if prop_meta.get("unwrapped", False): - # unwrapped array could either use prop items meta/prop meta - if prop_meta.get("itemsName"): - xml_name = prop_meta.get("itemsName") - xml_ns = prop_meta.get("itemNs") - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - items = args[0].findall(xml_name) # pyright: ignore - if len(items) > 0: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, items) - continue - - # text element is primitive type - if prop_meta.get("text", False): - if args[0].text is not None: - dict_to_pass[rf._rest_name] = _deserialize(rf._type, args[0].text) - continue - - # wrapped element could be normal property or array, it should only have one element - item = args[0].find(xml_name) - if item is not None: - existed_attr_keys.append(xml_name) - dict_to_pass[rf._rest_name] = _deserialize(rf._type, item) - - # rest thing is additional properties - for e in args[0]: - if e.tag not in existed_attr_keys: - dict_to_pass[e.tag] = _convert_element(e) - else: - dict_to_pass.update( - {k: _create_value(_get_rest_field(self._attr_to_rest_field, k), v) for k, v in args[0].items()} - ) - else: - non_attr_kwargs = [k for k in kwargs if k not in self._attr_to_rest_field] - if non_attr_kwargs: - # actual type errors only throw the first wrong keyword arg they see, so following that. - raise TypeError(f"{class_name}.__init__() got an unexpected keyword argument '{non_attr_kwargs[0]}'") - dict_to_pass.update( - { - self._attr_to_rest_field[k]._rest_name: _create_value(self._attr_to_rest_field[k], v) - for k, v in kwargs.items() - if v is not None - } - ) - super().__init__(dict_to_pass) - - def copy(self) -> "Model": - return Model(self.__dict__) - - def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: - if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated: - # we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping', - # 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object' - mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order - attr_to_rest_field: typing.Dict[str, _RestField] = { # map attribute name to rest_field property - k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type") - } - annotations = { - k: v - for mro_class in mros - if hasattr(mro_class, "__annotations__") - for k, v in mro_class.__annotations__.items() - } - for attr, rf in attr_to_rest_field.items(): - rf._module = cls.__module__ - if not rf._type: - rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None)) - if not rf._rest_name_input: - rf._rest_name_input = attr - cls._attr_to_rest_field: typing.Dict[str, _RestField] = dict(attr_to_rest_field.items()) - cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") - - return super().__new__(cls) # pylint: disable=no-value-for-parameter - - def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None: - for base in cls.__bases__: - if hasattr(base, "__mapping__"): - base.__mapping__[discriminator or cls.__name__] = cls # type: ignore - - @classmethod - def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: - for v in cls.__dict__.values(): - if isinstance(v, _RestField) and v._is_discriminator and v._rest_name not in exist_discriminators: - return v - return None - - @classmethod - def _deserialize(cls, data, exist_discriminators): - if not hasattr(cls, "__mapping__"): - return cls(data) - discriminator = cls._get_discriminator(exist_discriminators) - if discriminator is None: - return cls(data) - exist_discriminators.append(discriminator._rest_name) - if isinstance(data, ET.Element): - model_meta = getattr(cls, "_xml", {}) - prop_meta = getattr(discriminator, "_xml", {}) - xml_name = prop_meta.get("name", discriminator._rest_name) - xml_ns = prop_meta.get("ns", model_meta.get("ns", None)) - if xml_ns: - xml_name = "{" + xml_ns + "}" + xml_name - - if data.get(xml_name) is not None: - discriminator_value = data.get(xml_name) - else: - discriminator_value = data.find(xml_name).text # pyright: ignore - else: - discriminator_value = data.get(discriminator._rest_name) - mapped_cls = cls.__mapping__.get(discriminator_value, cls) # pyright: ignore - return mapped_cls._deserialize(data, exist_discriminators) - - def as_dict(self, *, exclude_readonly: bool = False) -> typing.Dict[str, typing.Any]: - """Return a dict that can be turned into json using json.dump. - - :keyword bool exclude_readonly: Whether to remove the readonly properties. - :returns: A dict JSON compatible object - :rtype: dict - """ - - result = {} - readonly_props = [] - if exclude_readonly: - readonly_props = [p._rest_name for p in self._attr_to_rest_field.values() if _is_readonly(p)] - for k, v in self.items(): - if exclude_readonly and k in readonly_props: # pyright: ignore - continue - is_multipart_file_input = False - try: - is_multipart_file_input = next( - rf for rf in self._attr_to_rest_field.values() if rf._rest_name == k - )._is_multipart_file_input - except StopIteration: - pass - result[k] = v if is_multipart_file_input else Model._as_dict_value(v, exclude_readonly=exclude_readonly) - return result - - @staticmethod - def _as_dict_value(v: typing.Any, exclude_readonly: bool = False) -> typing.Any: - if v is None or isinstance(v, _Null): - return None - if isinstance(v, (list, tuple, set)): - return type(v)(Model._as_dict_value(x, exclude_readonly=exclude_readonly) for x in v) - if isinstance(v, dict): - return {dk: Model._as_dict_value(dv, exclude_readonly=exclude_readonly) for dk, dv in v.items()} - return v.as_dict(exclude_readonly=exclude_readonly) if hasattr(v, "as_dict") else v - - -def _deserialize_model(model_deserializer: typing.Optional[typing.Callable], obj): - if _is_model(obj): - return obj - return _deserialize(model_deserializer, obj) - - -def _deserialize_with_optional(if_obj_deserializer: typing.Optional[typing.Callable], obj): - if obj is None: - return obj - return _deserialize_with_callable(if_obj_deserializer, obj) - - -def _deserialize_with_union(deserializers, obj): - for deserializer in deserializers: - try: - return _deserialize(deserializer, obj) - except DeserializationError: - pass - raise DeserializationError() - - -def _deserialize_dict( - value_deserializer: typing.Optional[typing.Callable], - module: typing.Optional[str], - obj: typing.Dict[typing.Any, typing.Any], -): - if obj is None: - return obj - if isinstance(obj, ET.Element): - obj = {child.tag: child for child in obj} - return {k: _deserialize(value_deserializer, v, module) for k, v in obj.items()} - - -def _deserialize_multiple_sequence( - entry_deserializers: typing.List[typing.Optional[typing.Callable]], - module: typing.Optional[str], - obj, -): - if obj is None: - return obj - return type(obj)(_deserialize(deserializer, entry, module) for entry, deserializer in zip(obj, entry_deserializers)) - - -def _deserialize_sequence( - deserializer: typing.Optional[typing.Callable], - module: typing.Optional[str], - obj, -): - if obj is None: - return obj - if isinstance(obj, ET.Element): - obj = list(obj) - return type(obj)(_deserialize(deserializer, entry, module) for entry in obj) - - -def _sorted_annotations(types: typing.List[typing.Any]) -> typing.List[typing.Any]: - return sorted( - types, - key=lambda x: hasattr(x, "__name__") and x.__name__.lower() in ("str", "float", "int", "bool"), - ) - - -def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-return-statements, too-many-branches - annotation: typing.Any, - module: typing.Optional[str], - rf: typing.Optional["_RestField"] = None, -) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: - if not annotation: - return None - - # is it a type alias? - if isinstance(annotation, str): - if module is not None: - annotation = _get_type_alias_type(module, annotation) - - # is it a forward ref / in quotes? - if isinstance(annotation, (str, typing.ForwardRef)): - try: - model_name = annotation.__forward_arg__ # type: ignore - except AttributeError: - model_name = annotation - if module is not None: - annotation = _get_model(module, model_name) # type: ignore - - try: - if module and _is_model(annotation): - if rf: - rf._is_model = True - - return functools.partial(_deserialize_model, annotation) # pyright: ignore - except Exception: - pass - - # is it a literal? - try: - if annotation.__origin__ is typing.Literal: # pyright: ignore - return None - except AttributeError: - pass - - # is it optional? - try: - if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore - if len(annotation.__args__) <= 2: # pyright: ignore - if_obj_deserializer = _get_deserialize_callable_from_annotation( - next(a for a in annotation.__args__ if a != type(None)), module, rf # pyright: ignore - ) - - return functools.partial(_deserialize_with_optional, if_obj_deserializer) - # the type is Optional[Union[...]], we need to remove the None type from the Union - annotation_copy = copy.copy(annotation) - annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a != type(None)] # pyright: ignore - return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) - except AttributeError: - pass - - # is it union? - if getattr(annotation, "__origin__", None) is typing.Union: - # initial ordering is we make `string` the last deserialization option, because it is often them most generic - deserializers = [ - _get_deserialize_callable_from_annotation(arg, module, rf) - for arg in _sorted_annotations(annotation.__args__) # pyright: ignore - ] - - return functools.partial(_deserialize_with_union, deserializers) - - try: - if annotation._name == "Dict": # pyright: ignore - value_deserializer = _get_deserialize_callable_from_annotation( - annotation.__args__[1], module, rf # pyright: ignore - ) - - return functools.partial( - _deserialize_dict, - value_deserializer, - module, - ) - except (AttributeError, IndexError): - pass - try: - if annotation._name in ["List", "Set", "Tuple", "Sequence"]: # pyright: ignore - if len(annotation.__args__) > 1: # pyright: ignore - entry_deserializers = [ - _get_deserialize_callable_from_annotation(dt, module, rf) - for dt in annotation.__args__ # pyright: ignore - ] - return functools.partial(_deserialize_multiple_sequence, entry_deserializers, module) - deserializer = _get_deserialize_callable_from_annotation( - annotation.__args__[0], module, rf # pyright: ignore - ) - - return functools.partial(_deserialize_sequence, deserializer, module) - except (TypeError, IndexError, AttributeError, SyntaxError): - pass - - def _deserialize_default( - deserializer, - obj, - ): - if obj is None: - return obj - try: - return _deserialize_with_callable(deserializer, obj) - except Exception: - pass - return obj - - if get_deserializer(annotation, rf): - return functools.partial(_deserialize_default, get_deserializer(annotation, rf)) - - return functools.partial(_deserialize_default, annotation) - - -def _deserialize_with_callable( - deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]], - value: typing.Any, -): # pylint: disable=too-many-return-statements - try: - if value is None or isinstance(value, _Null): - return None - if isinstance(value, ET.Element): - if deserializer is str: - return value.text or "" - if deserializer is int: - return int(value.text) if value.text else None - if deserializer is float: - return float(value.text) if value.text else None - if deserializer is bool: - return value.text == "true" if value.text else None - if deserializer is None: - return value - if deserializer in [int, float, bool]: - return deserializer(value) - if isinstance(deserializer, CaseInsensitiveEnumMeta): - try: - return deserializer(value) - except ValueError: - # for unknown value, return raw value - return value - if isinstance(deserializer, type) and issubclass(deserializer, Model): - return deserializer._deserialize(value, []) - return typing.cast(typing.Callable[[typing.Any], typing.Any], deserializer)(value) - except Exception as e: - raise DeserializationError() from e - - -def _deserialize( - deserializer: typing.Any, - value: typing.Any, - module: typing.Optional[str] = None, - rf: typing.Optional["_RestField"] = None, - format: typing.Optional[str] = None, -) -> typing.Any: - if isinstance(value, PipelineResponse): - value = value.http_response.json() - if rf is None and format: - rf = _RestField(format=format) - if not isinstance(deserializer, functools.partial): - deserializer = _get_deserialize_callable_from_annotation(deserializer, module, rf) - return _deserialize_with_callable(deserializer, value) - - -def _failsafe_deserialize( - deserializer: typing.Any, - value: typing.Any, - module: typing.Optional[str] = None, - rf: typing.Optional["_RestField"] = None, - format: typing.Optional[str] = None, -) -> typing.Any: - try: - return _deserialize(deserializer, value, module, rf, format) - except DeserializationError: - _LOGGER.warning( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - -def _failsafe_deserialize_xml( - deserializer: typing.Any, - value: typing.Any, -) -> typing.Any: - try: - return _deserialize_xml(deserializer, value) - except DeserializationError: - _LOGGER.warning( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - -class _RestField: - def __init__( - self, - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - is_discriminator: bool = False, - visibility: typing.Optional[typing.List[str]] = None, - default: typing.Any = _UNSET, - format: typing.Optional[str] = None, - is_multipart_file_input: bool = False, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, - ): - self._type = type - self._rest_name_input = name - self._module: typing.Optional[str] = None - self._is_discriminator = is_discriminator - self._visibility = visibility - self._is_model = False - self._default = default - self._format = format - self._is_multipart_file_input = is_multipart_file_input - self._xml = xml if xml is not None else {} - - @property - def _class_type(self) -> typing.Any: - return getattr(self._type, "args", [None])[0] - - @property - def _rest_name(self) -> str: - if self._rest_name_input is None: - raise ValueError("Rest name was never set") - return self._rest_name_input - - def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin - # by this point, type and rest_name will have a value bc we default - # them in __new__ of the Model class - item = obj.get(self._rest_name) - if item is None: - return item - if self._is_model: - return item - return _deserialize(self._type, _serialize(item, self._format), rf=self) - - def __set__(self, obj: Model, value) -> None: - if value is None: - # we want to wipe out entries if users set attr to None - try: - obj.__delitem__(self._rest_name) - except KeyError: - pass - return - if self._is_model: - if not _is_model(value): - value = _deserialize(self._type, value) - obj.__setitem__(self._rest_name, value) - return - obj.__setitem__(self._rest_name, _serialize(value, self._format)) - - def _get_deserialize_callable_from_annotation( - self, annotation: typing.Any - ) -> typing.Optional[typing.Callable[[typing.Any], typing.Any]]: - return _get_deserialize_callable_from_annotation(annotation, self._module, self) - - -def rest_field( - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[typing.List[str]] = None, - default: typing.Any = _UNSET, - format: typing.Optional[str] = None, - is_multipart_file_input: bool = False, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, -) -> typing.Any: - return _RestField( - name=name, - type=type, - visibility=visibility, - default=default, - format=format, - is_multipart_file_input=is_multipart_file_input, - xml=xml, - ) - - -def rest_discriminator( - *, - name: typing.Optional[str] = None, - type: typing.Optional[typing.Callable] = None, # pylint: disable=redefined-builtin - visibility: typing.Optional[typing.List[str]] = None, - xml: typing.Optional[typing.Dict[str, typing.Any]] = None, -) -> typing.Any: - return _RestField(name=name, type=type, is_discriminator=True, visibility=visibility, xml=xml) - - -def serialize_xml(model: Model, exclude_readonly: bool = False) -> str: - """Serialize a model to XML. - - :param Model model: The model to serialize. - :param bool exclude_readonly: Whether to exclude readonly properties. - :returns: The XML representation of the model. - :rtype: str - """ - return ET.tostring(_get_element(model, exclude_readonly), encoding="unicode") # type: ignore - - -def _get_element( - o: typing.Any, - exclude_readonly: bool = False, - parent_meta: typing.Optional[typing.Dict[str, typing.Any]] = None, - wrapped_element: typing.Optional[ET.Element] = None, -) -> typing.Union[ET.Element, typing.List[ET.Element]]: - if _is_model(o): - model_meta = getattr(o, "_xml", {}) - - # if prop is a model, then use the prop element directly, else generate a wrapper of model - if wrapped_element is None: - wrapped_element = _create_xml_element( - model_meta.get("name", o.__class__.__name__), - model_meta.get("prefix"), - model_meta.get("ns"), - ) - - readonly_props = [] - if exclude_readonly: - readonly_props = [p._rest_name for p in o._attr_to_rest_field.values() if _is_readonly(p)] - - for k, v in o.items(): - # do not serialize readonly properties - if exclude_readonly and k in readonly_props: - continue - - prop_rest_field = _get_rest_field(o._attr_to_rest_field, k) - if prop_rest_field: - prop_meta = getattr(prop_rest_field, "_xml").copy() - # use the wire name as xml name if no specific name is set - if prop_meta.get("name") is None: - prop_meta["name"] = k - else: - # additional properties will not have rest field, use the wire name as xml name - prop_meta = {"name": k} - - # if no ns for prop, use model's - if prop_meta.get("ns") is None and model_meta.get("ns"): - prop_meta["ns"] = model_meta.get("ns") - prop_meta["prefix"] = model_meta.get("prefix") - - if prop_meta.get("unwrapped", False): - # unwrapped could only set on array - wrapped_element.extend(_get_element(v, exclude_readonly, prop_meta)) - elif prop_meta.get("text", False): - # text could only set on primitive type - wrapped_element.text = _get_primitive_type_value(v) - elif prop_meta.get("attribute", False): - xml_name = prop_meta.get("name", k) - if prop_meta.get("ns"): - ET.register_namespace(prop_meta.get("prefix"), prop_meta.get("ns")) # pyright: ignore - xml_name = "{" + prop_meta.get("ns") + "}" + xml_name # pyright: ignore - # attribute should be primitive type - wrapped_element.set(xml_name, _get_primitive_type_value(v)) - else: - # other wrapped prop element - wrapped_element.append(_get_wrapped_element(v, exclude_readonly, prop_meta)) - return wrapped_element - if isinstance(o, list): - return [_get_element(x, exclude_readonly, parent_meta) for x in o] # type: ignore - if isinstance(o, dict): - result = [] - for k, v in o.items(): - result.append( - _get_wrapped_element( - v, - exclude_readonly, - { - "name": k, - "ns": parent_meta.get("ns") if parent_meta else None, - "prefix": parent_meta.get("prefix") if parent_meta else None, - }, - ) - ) - return result - - # primitive case need to create element based on parent_meta - if parent_meta: - return _get_wrapped_element( - o, - exclude_readonly, - { - "name": parent_meta.get("itemsName", parent_meta.get("name")), - "prefix": parent_meta.get("itemsPrefix", parent_meta.get("prefix")), - "ns": parent_meta.get("itemsNs", parent_meta.get("ns")), - }, - ) - - raise ValueError("Could not serialize value into xml: " + o) - - -def _get_wrapped_element( - v: typing.Any, - exclude_readonly: bool, - meta: typing.Optional[typing.Dict[str, typing.Any]], -) -> ET.Element: - wrapped_element = _create_xml_element( - meta.get("name") if meta else None, meta.get("prefix") if meta else None, meta.get("ns") if meta else None - ) - if isinstance(v, (dict, list)): - wrapped_element.extend(_get_element(v, exclude_readonly, meta)) - elif _is_model(v): - _get_element(v, exclude_readonly, meta, wrapped_element) - else: - wrapped_element.text = _get_primitive_type_value(v) - return wrapped_element - - -def _get_primitive_type_value(v) -> str: - if v is True: - return "true" - if v is False: - return "false" - if isinstance(v, _Null): - return "" - return str(v) - - -def _create_xml_element(tag, prefix=None, ns=None): - if prefix and ns: - ET.register_namespace(prefix, ns) - if ns: - return ET.Element("{" + ns + "}" + tag) - return ET.Element(tag) - - -def _deserialize_xml( - deserializer: typing.Any, - value: str, -) -> typing.Any: - element = ET.fromstring(value) # nosec - return _deserialize(deserializer, element) - - -def _convert_element(e: ET.Element): - # dict case - if len(e.attrib) > 0 or len({child.tag for child in e}) > 1: - dict_result: typing.Dict[str, typing.Any] = {} - for child in e: - if dict_result.get(child.tag) is not None: - if isinstance(dict_result[child.tag], list): - dict_result[child.tag].append(_convert_element(child)) - else: - dict_result[child.tag] = [dict_result[child.tag], _convert_element(child)] - else: - dict_result[child.tag] = _convert_element(child) - dict_result.update(e.attrib) - return dict_result - # array case - if len(e) > 0: - array_result: typing.List[typing.Any] = [] - for child in e: - array_result.append(_convert_element(child)) - return array_result - # primitive case - return e.text diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_operations.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_operations.py index de4199053d26..f26baaf2b3b5 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_operations.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_operations.py @@ -49,7 +49,7 @@ def build_key_vault_create_key_request(key_name: str, **kwargs: Any) -> HttpRequ _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -75,7 +75,7 @@ def build_key_vault_rotate_key_request(key_name: str, **kwargs: Any) -> HttpRequ _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -100,7 +100,7 @@ def build_key_vault_import_key_request(key_name: str, **kwargs: Any) -> HttpRequ _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -126,7 +126,7 @@ def build_key_vault_delete_key_request(key_name: str, **kwargs: Any) -> HttpRequ _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -151,7 +151,7 @@ def build_key_vault_update_key_request(key_name: str, key_version: str, **kwargs _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -178,7 +178,7 @@ def build_key_vault_get_key_request(key_name: str, key_version: str, **kwargs: A _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -205,7 +205,7 @@ def build_key_vault_get_key_versions_request( _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -231,7 +231,7 @@ def build_key_vault_get_keys_request(*, maxresults: Optional[int] = None, **kwar _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -252,7 +252,7 @@ def build_key_vault_backup_key_request(key_name: str, **kwargs: Any) -> HttpRequ _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -277,7 +277,7 @@ def build_key_vault_restore_key_request(**kwargs: Any) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -299,7 +299,7 @@ def build_key_vault_encrypt_request(key_name: str, key_version: str, **kwargs: A _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -327,7 +327,7 @@ def build_key_vault_decrypt_request(key_name: str, key_version: str, **kwargs: A _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -355,7 +355,7 @@ def build_key_vault_sign_request(key_name: str, key_version: str, **kwargs: Any) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -383,7 +383,7 @@ def build_key_vault_verify_request(key_name: str, key_version: str, **kwargs: An _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -411,7 +411,7 @@ def build_key_vault_wrap_key_request(key_name: str, key_version: str, **kwargs: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -434,12 +434,70 @@ def build_key_vault_wrap_key_request(key_name: str, key_version: str, **kwargs: return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) +def build_key_vault_secure_wrap_key_request(key_name: str, key_version: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/keys/{key-name}/{key-version}/securewrapkey" + path_format_arguments = { + "key-name": _SERIALIZER.url("key_name", key_name, "str"), + "key-version": _SERIALIZER.url("key_version", key_version, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) + + +def build_key_vault_secure_unwrap_key_request( # pylint: disable=name-too-long + key_name: str, key_version: str, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/keys/{key-name}/{key-version}/secureunwrapkey" + path_format_arguments = { + "key-name": _SERIALIZER.url("key_name", key_name, "str"), + "key-version": _SERIALIZER.url("key_version", key_version, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + _params["api-version"] = _SERIALIZER.query("api_version", api_version, "str") + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header("content_type", content_type, "str") + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, params=_params, headers=_headers, **kwargs) + + def build_key_vault_unwrap_key_request(key_name: str, key_version: str, **kwargs: Any) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -467,7 +525,7 @@ def build_key_vault_release_request(key_name: str, key_version: str, **kwargs: A _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -494,7 +552,7 @@ def build_key_vault_get_deleted_keys_request(*, maxresults: Optional[int] = None _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -515,7 +573,7 @@ def build_key_vault_get_deleted_key_request(key_name: str, **kwargs: Any) -> Htt _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -540,7 +598,7 @@ def build_key_vault_purge_deleted_key_request( # pylint: disable=name-too-long ) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) # Construct URL _url = "/deletedkeys/{key-name}" path_format_arguments = { @@ -561,7 +619,7 @@ def build_key_vault_recover_deleted_key_request( # pylint: disable=name-too-lon _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -587,7 +645,7 @@ def build_key_vault_get_key_rotation_policy_request( # pylint: disable=name-too _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -614,7 +672,7 @@ def build_key_vault_update_key_rotation_policy_request( # pylint: disable=name- _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -641,7 +699,7 @@ def build_key_vault_get_random_bytes_request(**kwargs: Any) -> HttpRequest: _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -664,7 +722,7 @@ def build_key_vault_get_key_attestation_request( # pylint: disable=name-too-lon _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) - api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2025-07-01")) + api_version: str = kwargs.pop("api_version", _params.pop("api-version", "2026-03-01-preview")) accept = _headers.pop("Accept", "application/json") # Construct URL @@ -1495,7 +1553,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -1593,7 +1654,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -2873,6 +2937,428 @@ def wrap_key( return deserialized # type: ignore + @overload + def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: _models.SecureKeyWrapOperationParameters, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyWrapOperationParameters + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Required. + :type parameters: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Required. + :type parameters: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace + @api_version_validation( + method_added_on="2026-01-01-preview", + params_added_on={"2026-01-01-preview": ["key_name", "key_version", "content_type", "accept", "api_version"]}, + api_versions_list=["2026-01-01-preview", "2026-03-01-preview"], + ) + def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: Union[_models.SecureKeyWrapOperationParameters, JSON, IO[bytes]], + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Is one of the following types: + SecureKeyWrapOperationParameters, JSON, IO[bytes] Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyWrapOperationParameters or + JSON or IO[bytes] + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.SecureKeyOperationResult] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(parameters, (IOBase, bytes)): + _content = parameters + else: + _content = json.dumps(parameters, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_key_vault_secure_wrap_key_request( + key_name=key_name, + key_version=key_version, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "vaultBaseUrl": self._serialize.url( + "self._config.vault_base_url", self._config.vault_base_url, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.SecureKeyOperationResult, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @overload + def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: _models.SecureKeyUnWrapOperationParameters, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyUnWrapOperationParameters + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Required. + :type parameters: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Required. + :type parameters: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace + @api_version_validation( + method_added_on="2026-01-01-preview", + params_added_on={"2026-01-01-preview": ["key_name", "key_version", "content_type", "accept", "api_version"]}, + api_versions_list=["2026-01-01-preview", "2026-03-01-preview"], + ) + def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: Union[_models.SecureKeyUnWrapOperationParameters, JSON, IO[bytes]], + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Is one of the following types: + SecureKeyUnWrapOperationParameters, JSON, IO[bytes] Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyUnWrapOperationParameters or + JSON or IO[bytes] + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.SecureKeyOperationResult] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(parameters, (IOBase, bytes)): + _content = parameters + else: + _content = json.dumps(parameters, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_key_vault_secure_unwrap_key_request( + key_name=key_name, + key_version=key_version, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "vaultBaseUrl": self._serialize.url( + "self._config.vault_base_url", self._config.vault_base_url, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.SecureKeyOperationResult, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + @overload def unwrap_key( self, @@ -3304,7 +3790,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -3911,7 +4400,14 @@ def get_random_bytes( @api_version_validation( method_added_on="7.6-preview.2", params_added_on={"7.6-preview.2": ["key_name", "key_version", "accept", "api_version"]}, - api_versions_list=["7.6-preview.2", "7.6", "2025-06-01-preview", "2025-07-01"], + api_versions_list=[ + "7.6-preview.2", + "7.6", + "2025-06-01-preview", + "2025-07-01", + "2026-01-01-preview", + "2026-03-01-preview", + ], ) def get_key_attestation(self, key_name: str, key_version: str, **kwargs: Any) -> _models.KeyBundle: """Gets the public part of a stored key along with its attestation blob. diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_patch.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_patch.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_operations/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_patch.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_patch.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_serialization.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_serialization.py deleted file mode 100644 index ef86f1415163..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_serialization.py +++ /dev/null @@ -1,2050 +0,0 @@ -# pylint: disable=too-many-lines,line-too-long,useless-suppression -# -------------------------------------------------------------------------- -# -# Copyright (c) Microsoft Corporation. All rights reserved. -# -# The MIT License (MIT) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the ""Software""), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. -# -# -------------------------------------------------------------------------- - -# pyright: reportUnnecessaryTypeIgnoreComment=false - -from base64 import b64decode, b64encode -import calendar -import datetime -import decimal -import email -from enum import Enum -import json -import logging -import re -import sys -import codecs -from typing import ( - Dict, - Any, - cast, - Optional, - Union, - AnyStr, - IO, - Mapping, - Callable, - MutableMapping, - List, -) - -try: - from urllib import quote # type: ignore -except ImportError: - from urllib.parse import quote -import xml.etree.ElementTree as ET - -import isodate # type: ignore -from typing_extensions import Self - -from azure.core.exceptions import DeserializationError, SerializationError -from azure.core.serialization import NULL as CoreNull - -_BOM = codecs.BOM_UTF8.decode(encoding="utf-8") - -JSON = MutableMapping[str, Any] - - -class RawDeserializer: - - # Accept "text" because we're open minded people... - JSON_REGEXP = re.compile(r"^(application|text)/([a-z+.]+\+)?json$") - - # Name used in context - CONTEXT_NAME = "deserialized_data" - - @classmethod - def deserialize_from_text(cls, data: Optional[Union[AnyStr, IO]], content_type: Optional[str] = None) -> Any: - """Decode data according to content-type. - - Accept a stream of data as well, but will be load at once in memory for now. - - If no content-type, will return the string version (not bytes, not stream) - - :param data: Input, could be bytes or stream (will be decoded with UTF8) or text - :type data: str or bytes or IO - :param str content_type: The content type. - :return: The deserialized data. - :rtype: object - """ - if hasattr(data, "read"): - # Assume a stream - data = cast(IO, data).read() - - if isinstance(data, bytes): - data_as_str = data.decode(encoding="utf-8-sig") - else: - # Explain to mypy the correct type. - data_as_str = cast(str, data) - - # Remove Byte Order Mark if present in string - data_as_str = data_as_str.lstrip(_BOM) - - if content_type is None: - return data - - if cls.JSON_REGEXP.match(content_type): - try: - return json.loads(data_as_str) - except ValueError as err: - raise DeserializationError("JSON is invalid: {}".format(err), err) from err - elif "xml" in (content_type or []): - try: - - try: - if isinstance(data, unicode): # type: ignore - # If I'm Python 2.7 and unicode XML will scream if I try a "fromstring" on unicode string - data_as_str = data_as_str.encode(encoding="utf-8") # type: ignore - except NameError: - pass - - return ET.fromstring(data_as_str) # nosec - except ET.ParseError as err: - # It might be because the server has an issue, and returned JSON with - # content-type XML.... - # So let's try a JSON load, and if it's still broken - # let's flow the initial exception - def _json_attemp(data): - try: - return True, json.loads(data) - except ValueError: - return False, None # Don't care about this one - - success, json_result = _json_attemp(data) - if success: - return json_result - # If i'm here, it's not JSON, it's not XML, let's scream - # and raise the last context in this block (the XML exception) - # The function hack is because Py2.7 messes up with exception - # context otherwise. - _LOGGER.critical("Wasn't XML not JSON, failing") - raise DeserializationError("XML is invalid") from err - elif content_type.startswith("text/"): - return data_as_str - raise DeserializationError("Cannot deserialize content-type: {}".format(content_type)) - - @classmethod - def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], headers: Mapping) -> Any: - """Deserialize from HTTP response. - - Use bytes and headers to NOT use any requests/aiohttp or whatever - specific implementation. - Headers will tested for "content-type" - - :param bytes body_bytes: The body of the response. - :param dict headers: The headers of the response. - :returns: The deserialized data. - :rtype: object - """ - # Try to use content-type from headers if available - content_type = None - if "content-type" in headers: - content_type = headers["content-type"].split(";")[0].strip().lower() - # Ouch, this server did not declare what it sent... - # Let's guess it's JSON... - # Also, since Autorest was considering that an empty body was a valid JSON, - # need that test as well.... - else: - content_type = "application/json" - - if body_bytes: - return cls.deserialize_from_text(body_bytes, content_type) - return None - - -_LOGGER = logging.getLogger(__name__) - -try: - _long_type = long # type: ignore -except NameError: - _long_type = int - -TZ_UTC = datetime.timezone.utc - -_FLATTEN = re.compile(r"(? None: - self.additional_properties: Optional[Dict[str, Any]] = {} - for k in kwargs: # pylint: disable=consider-using-dict-items - if k not in self._attribute_map: - _LOGGER.warning("%s is not a known attribute of class %s and will be ignored", k, self.__class__) - elif k in self._validation and self._validation[k].get("readonly", False): - _LOGGER.warning("Readonly attribute %s will be ignored in class %s", k, self.__class__) - else: - setattr(self, k, kwargs[k]) - - def __eq__(self, other: Any) -> bool: - """Compare objects by comparing all attributes. - - :param object other: The object to compare - :returns: True if objects are equal - :rtype: bool - """ - if isinstance(other, self.__class__): - return self.__dict__ == other.__dict__ - return False - - def __ne__(self, other: Any) -> bool: - """Compare objects by comparing all attributes. - - :param object other: The object to compare - :returns: True if objects are not equal - :rtype: bool - """ - return not self.__eq__(other) - - def __str__(self) -> str: - return str(self.__dict__) - - @classmethod - def enable_additional_properties_sending(cls) -> None: - cls._attribute_map["additional_properties"] = {"key": "", "type": "{object}"} - - @classmethod - def is_xml_model(cls) -> bool: - try: - cls._xml_map # type: ignore - except AttributeError: - return False - return True - - @classmethod - def _create_xml_node(cls): - """Create XML node. - - :returns: The XML node - :rtype: xml.etree.ElementTree.Element - """ - try: - xml_map = cls._xml_map # type: ignore - except AttributeError: - xml_map = {} - - return _create_xml_node(xml_map.get("name", cls.__name__), xml_map.get("prefix", None), xml_map.get("ns", None)) - - def serialize(self, keep_readonly: bool = False, **kwargs: Any) -> JSON: - """Return the JSON that would be sent to server from this model. - - This is an alias to `as_dict(full_restapi_key_transformer, keep_readonly=False)`. - - If you want XML serialization, you can pass the kwargs is_xml=True. - - :param bool keep_readonly: If you want to serialize the readonly attributes - :returns: A dict JSON compatible object - :rtype: dict - """ - serializer = Serializer(self._infer_class_models()) - return serializer._serialize( # type: ignore # pylint: disable=protected-access - self, keep_readonly=keep_readonly, **kwargs - ) - - def as_dict( - self, - keep_readonly: bool = True, - key_transformer: Callable[[str, Dict[str, Any], Any], Any] = attribute_transformer, - **kwargs: Any - ) -> JSON: - """Return a dict that can be serialized using json.dump. - - Advanced usage might optionally use a callback as parameter: - - .. code::python - - def my_key_transformer(key, attr_desc, value): - return key - - Key is the attribute name used in Python. Attr_desc - is a dict of metadata. Currently contains 'type' with the - msrest type and 'key' with the RestAPI encoded key. - Value is the current value in this object. - - The string returned will be used to serialize the key. - If the return type is a list, this is considered hierarchical - result dict. - - See the three examples in this file: - - - attribute_transformer - - full_restapi_key_transformer - - last_restapi_key_transformer - - If you want XML serialization, you can pass the kwargs is_xml=True. - - :param bool keep_readonly: If you want to serialize the readonly attributes - :param function key_transformer: A key transformer function. - :returns: A dict JSON compatible object - :rtype: dict - """ - serializer = Serializer(self._infer_class_models()) - return serializer._serialize( # type: ignore # pylint: disable=protected-access - self, key_transformer=key_transformer, keep_readonly=keep_readonly, **kwargs - ) - - @classmethod - def _infer_class_models(cls): - try: - str_models = cls.__module__.rsplit(".", 1)[0] - models = sys.modules[str_models] - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - if cls.__name__ not in client_models: - raise ValueError("Not Autorest generated code") - except Exception: # pylint: disable=broad-exception-caught - # Assume it's not Autorest generated (tests?). Add ourselves as dependencies. - client_models = {cls.__name__: cls} - return client_models - - @classmethod - def deserialize(cls, data: Any, content_type: Optional[str] = None) -> Self: - """Parse a str using the RestAPI syntax and return a model. - - :param str data: A str using RestAPI structure. JSON by default. - :param str content_type: JSON by default, set application/xml if XML. - :returns: An instance of this model - :raises DeserializationError: if something went wrong - :rtype: Self - """ - deserializer = Deserializer(cls._infer_class_models()) - return deserializer(cls.__name__, data, content_type=content_type) # type: ignore - - @classmethod - def from_dict( - cls, - data: Any, - key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, - content_type: Optional[str] = None, - ) -> Self: - """Parse a dict using given key extractor return a model. - - By default consider key - extractors (rest_key_case_insensitive_extractor, attribute_key_case_insensitive_extractor - and last_rest_key_case_insensitive_extractor) - - :param dict data: A dict using RestAPI structure - :param function key_extractors: A key extractor function. - :param str content_type: JSON by default, set application/xml if XML. - :returns: An instance of this model - :raises: DeserializationError if something went wrong - :rtype: Self - """ - deserializer = Deserializer(cls._infer_class_models()) - deserializer.key_extractors = ( # type: ignore - [ # type: ignore - attribute_key_case_insensitive_extractor, - rest_key_case_insensitive_extractor, - last_rest_key_case_insensitive_extractor, - ] - if key_extractors is None - else key_extractors - ) - return deserializer(cls.__name__, data, content_type=content_type) # type: ignore - - @classmethod - def _flatten_subtype(cls, key, objects): - if "_subtype_map" not in cls.__dict__: - return {} - result = dict(cls._subtype_map[key]) - for valuetype in cls._subtype_map[key].values(): - result.update(objects[valuetype]._flatten_subtype(key, objects)) # pylint: disable=protected-access - return result - - @classmethod - def _classify(cls, response, objects): - """Check the class _subtype_map for any child classes. - We want to ignore any inherited _subtype_maps. - - :param dict response: The initial data - :param dict objects: The class objects - :returns: The class to be used - :rtype: class - """ - for subtype_key in cls.__dict__.get("_subtype_map", {}).keys(): - subtype_value = None - - if not isinstance(response, ET.Element): - rest_api_response_key = cls._get_rest_key_parts(subtype_key)[-1] - subtype_value = response.get(rest_api_response_key, None) or response.get(subtype_key, None) - else: - subtype_value = xml_key_extractor(subtype_key, cls._attribute_map[subtype_key], response) - if subtype_value: - # Try to match base class. Can be class name only - # (bug to fix in Autorest to support x-ms-discriminator-name) - if cls.__name__ == subtype_value: - return cls - flatten_mapping_type = cls._flatten_subtype(subtype_key, objects) - try: - return objects[flatten_mapping_type[subtype_value]] # type: ignore - except KeyError: - _LOGGER.warning( - "Subtype value %s has no mapping, use base class %s.", - subtype_value, - cls.__name__, - ) - break - else: - _LOGGER.warning("Discriminator %s is absent or null, use base class %s.", subtype_key, cls.__name__) - break - return cls - - @classmethod - def _get_rest_key_parts(cls, attr_key): - """Get the RestAPI key of this attr, split it and decode part - :param str attr_key: Attribute key must be in attribute_map. - :returns: A list of RestAPI part - :rtype: list - """ - rest_split_key = _FLATTEN.split(cls._attribute_map[attr_key]["key"]) - return [_decode_attribute_map_key(key_part) for key_part in rest_split_key] - - -def _decode_attribute_map_key(key): - """This decode a key in an _attribute_map to the actual key we want to look at - inside the received data. - - :param str key: A key string from the generated code - :returns: The decoded key - :rtype: str - """ - return key.replace("\\.", ".") - - -class Serializer: # pylint: disable=too-many-public-methods - """Request object model serializer.""" - - basic_types = {str: "str", int: "int", bool: "bool", float: "float"} - - _xml_basic_types_serializers = {"bool": lambda x: str(x).lower()} - days = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"} - months = { - 1: "Jan", - 2: "Feb", - 3: "Mar", - 4: "Apr", - 5: "May", - 6: "Jun", - 7: "Jul", - 8: "Aug", - 9: "Sep", - 10: "Oct", - 11: "Nov", - 12: "Dec", - } - validation = { - "min_length": lambda x, y: len(x) < y, - "max_length": lambda x, y: len(x) > y, - "minimum": lambda x, y: x < y, - "maximum": lambda x, y: x > y, - "minimum_ex": lambda x, y: x <= y, - "maximum_ex": lambda x, y: x >= y, - "min_items": lambda x, y: len(x) < y, - "max_items": lambda x, y: len(x) > y, - "pattern": lambda x, y: not re.match(y, x, re.UNICODE), - "unique": lambda x, y: len(x) != len(set(x)), - "multiple": lambda x, y: x % y != 0, - } - - def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: - self.serialize_type = { - "iso-8601": Serializer.serialize_iso, - "rfc-1123": Serializer.serialize_rfc, - "unix-time": Serializer.serialize_unix, - "duration": Serializer.serialize_duration, - "date": Serializer.serialize_date, - "time": Serializer.serialize_time, - "decimal": Serializer.serialize_decimal, - "long": Serializer.serialize_long, - "bytearray": Serializer.serialize_bytearray, - "base64": Serializer.serialize_base64, - "object": self.serialize_object, - "[]": self.serialize_iter, - "{}": self.serialize_dict, - } - self.dependencies: Dict[str, type] = dict(classes) if classes else {} - self.key_transformer = full_restapi_key_transformer - self.client_side_validation = True - - def _serialize( # pylint: disable=too-many-nested-blocks, too-many-branches, too-many-statements, too-many-locals - self, target_obj, data_type=None, **kwargs - ): - """Serialize data into a string according to type. - - :param object target_obj: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str, dict - :raises SerializationError: if serialization fails. - :returns: The serialized data. - """ - key_transformer = kwargs.get("key_transformer", self.key_transformer) - keep_readonly = kwargs.get("keep_readonly", False) - if target_obj is None: - return None - - attr_name = None - class_name = target_obj.__class__.__name__ - - if data_type: - return self.serialize_data(target_obj, data_type, **kwargs) - - if not hasattr(target_obj, "_attribute_map"): - data_type = type(target_obj).__name__ - if data_type in self.basic_types.values(): - return self.serialize_data(target_obj, data_type, **kwargs) - - # Force "is_xml" kwargs if we detect a XML model - try: - is_xml_model_serialization = kwargs["is_xml"] - except KeyError: - is_xml_model_serialization = kwargs.setdefault("is_xml", target_obj.is_xml_model()) - - serialized = {} - if is_xml_model_serialization: - serialized = target_obj._create_xml_node() # pylint: disable=protected-access - try: - attributes = target_obj._attribute_map # pylint: disable=protected-access - for attr, attr_desc in attributes.items(): - attr_name = attr - if not keep_readonly and target_obj._validation.get( # pylint: disable=protected-access - attr_name, {} - ).get("readonly", False): - continue - - if attr_name == "additional_properties" and attr_desc["key"] == "": - if target_obj.additional_properties is not None: - serialized.update(target_obj.additional_properties) - continue - try: - - orig_attr = getattr(target_obj, attr) - if is_xml_model_serialization: - pass # Don't provide "transformer" for XML for now. Keep "orig_attr" - else: # JSON - keys, orig_attr = key_transformer(attr, attr_desc.copy(), orig_attr) - keys = keys if isinstance(keys, list) else [keys] - - kwargs["serialization_ctxt"] = attr_desc - new_attr = self.serialize_data(orig_attr, attr_desc["type"], **kwargs) - - if is_xml_model_serialization: - xml_desc = attr_desc.get("xml", {}) - xml_name = xml_desc.get("name", attr_desc["key"]) - xml_prefix = xml_desc.get("prefix", None) - xml_ns = xml_desc.get("ns", None) - if xml_desc.get("attr", False): - if xml_ns: - ET.register_namespace(xml_prefix, xml_ns) - xml_name = "{{{}}}{}".format(xml_ns, xml_name) - serialized.set(xml_name, new_attr) # type: ignore - continue - if xml_desc.get("text", False): - serialized.text = new_attr # type: ignore - continue - if isinstance(new_attr, list): - serialized.extend(new_attr) # type: ignore - elif isinstance(new_attr, ET.Element): - # If the down XML has no XML/Name, - # we MUST replace the tag with the local tag. But keeping the namespaces. - if "name" not in getattr(orig_attr, "_xml_map", {}): - splitted_tag = new_attr.tag.split("}") - if len(splitted_tag) == 2: # Namespace - new_attr.tag = "}".join([splitted_tag[0], xml_name]) - else: - new_attr.tag = xml_name - serialized.append(new_attr) # type: ignore - else: # That's a basic type - # Integrate namespace if necessary - local_node = _create_xml_node(xml_name, xml_prefix, xml_ns) - local_node.text = str(new_attr) - serialized.append(local_node) # type: ignore - else: # JSON - for k in reversed(keys): # type: ignore - new_attr = {k: new_attr} - - _new_attr = new_attr - _serialized = serialized - for k in keys: # type: ignore - if k not in _serialized: - _serialized.update(_new_attr) # type: ignore - _new_attr = _new_attr[k] # type: ignore - _serialized = _serialized[k] - except ValueError as err: - if isinstance(err, SerializationError): - raise - - except (AttributeError, KeyError, TypeError) as err: - msg = "Attribute {} in object {} cannot be serialized.\n{}".format(attr_name, class_name, str(target_obj)) - raise SerializationError(msg) from err - return serialized - - def body(self, data, data_type, **kwargs): - """Serialize data intended for a request body. - - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: dict - :raises SerializationError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized request body - """ - - # Just in case this is a dict - internal_data_type_str = data_type.strip("[]{}") - internal_data_type = self.dependencies.get(internal_data_type_str, None) - try: - is_xml_model_serialization = kwargs["is_xml"] - except KeyError: - if internal_data_type and issubclass(internal_data_type, Model): - is_xml_model_serialization = kwargs.setdefault("is_xml", internal_data_type.is_xml_model()) - else: - is_xml_model_serialization = False - if internal_data_type and not isinstance(internal_data_type, Enum): - try: - deserializer = Deserializer(self.dependencies) - # Since it's on serialization, it's almost sure that format is not JSON REST - # We're not able to deal with additional properties for now. - deserializer.additional_properties_detection = False - if is_xml_model_serialization: - deserializer.key_extractors = [ # type: ignore - attribute_key_case_insensitive_extractor, - ] - else: - deserializer.key_extractors = [ - rest_key_case_insensitive_extractor, - attribute_key_case_insensitive_extractor, - last_rest_key_case_insensitive_extractor, - ] - data = deserializer._deserialize(data_type, data) # pylint: disable=protected-access - except DeserializationError as err: - raise SerializationError("Unable to build a model: " + str(err)) from err - - return self._serialize(data, data_type, **kwargs) - - def url(self, name, data, data_type, **kwargs): - """Serialize data intended for a URL path. - - :param str name: The name of the URL path parameter. - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str - :returns: The serialized URL path - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - """ - try: - output = self.serialize_data(data, data_type, **kwargs) - if data_type == "bool": - output = json.dumps(output) - - if kwargs.get("skip_quote") is True: - output = str(output) - output = output.replace("{", quote("{")).replace("}", quote("}")) - else: - output = quote(str(output), safe="") - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return output - - def query(self, name, data, data_type, **kwargs): - """Serialize data intended for a URL query. - - :param str name: The name of the query parameter. - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str, list - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized query parameter - """ - try: - # Treat the list aside, since we don't want to encode the div separator - if data_type.startswith("["): - internal_data_type = data_type[1:-1] - do_quote = not kwargs.get("skip_quote", False) - return self.serialize_iter(data, internal_data_type, do_quote=do_quote, **kwargs) - - # Not a list, regular serialization - output = self.serialize_data(data, data_type, **kwargs) - if data_type == "bool": - output = json.dumps(output) - if kwargs.get("skip_quote") is True: - output = str(output) - else: - output = quote(str(output), safe="") - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return str(output) - - def header(self, name, data, data_type, **kwargs): - """Serialize data intended for a request header. - - :param str name: The name of the header. - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :rtype: str - :raises TypeError: if serialization fails. - :raises ValueError: if data is None - :returns: The serialized header - """ - try: - if data_type in ["[str]"]: - data = ["" if d is None else d for d in data] - - output = self.serialize_data(data, data_type, **kwargs) - if data_type == "bool": - output = json.dumps(output) - except SerializationError as exc: - raise TypeError("{} must be type {}.".format(name, data_type)) from exc - return str(output) - - def serialize_data(self, data, data_type, **kwargs): - """Serialize generic data according to supplied data type. - - :param object data: The data to be serialized. - :param str data_type: The type to be serialized from. - :raises AttributeError: if required data is None. - :raises ValueError: if data is None - :raises SerializationError: if serialization fails. - :returns: The serialized data. - :rtype: str, int, float, bool, dict, list - """ - if data is None: - raise ValueError("No value for given attribute") - - try: - if data is CoreNull: - return None - if data_type in self.basic_types.values(): - return self.serialize_basic(data, data_type, **kwargs) - - if data_type in self.serialize_type: - return self.serialize_type[data_type](data, **kwargs) - - # If dependencies is empty, try with current data class - # It has to be a subclass of Enum anyway - enum_type = self.dependencies.get(data_type, data.__class__) - if issubclass(enum_type, Enum): - return Serializer.serialize_enum(data, enum_obj=enum_type) - - iter_type = data_type[0] + data_type[-1] - if iter_type in self.serialize_type: - return self.serialize_type[iter_type](data, data_type[1:-1], **kwargs) - - except (ValueError, TypeError) as err: - msg = "Unable to serialize value: {!r} as type: {!r}." - raise SerializationError(msg.format(data, data_type)) from err - return self._serialize(data, **kwargs) - - @classmethod - def _get_custom_serializers(cls, data_type, **kwargs): # pylint: disable=inconsistent-return-statements - custom_serializer = kwargs.get("basic_types_serializers", {}).get(data_type) - if custom_serializer: - return custom_serializer - if kwargs.get("is_xml", False): - return cls._xml_basic_types_serializers.get(data_type) - - @classmethod - def serialize_basic(cls, data, data_type, **kwargs): - """Serialize basic builting data type. - Serializes objects to str, int, float or bool. - - Possible kwargs: - - basic_types_serializers dict[str, callable] : If set, use the callable as serializer - - is_xml bool : If set, use xml_basic_types_serializers - - :param obj data: Object to be serialized. - :param str data_type: Type of object in the iterable. - :rtype: str, int, float, bool - :return: serialized object - """ - custom_serializer = cls._get_custom_serializers(data_type, **kwargs) - if custom_serializer: - return custom_serializer(data) - if data_type == "str": - return cls.serialize_unicode(data) - return eval(data_type)(data) # nosec # pylint: disable=eval-used - - @classmethod - def serialize_unicode(cls, data): - """Special handling for serializing unicode strings in Py2. - Encode to UTF-8 if unicode, otherwise handle as a str. - - :param str data: Object to be serialized. - :rtype: str - :return: serialized object - """ - try: # If I received an enum, return its value - return data.value - except AttributeError: - pass - - try: - if isinstance(data, unicode): # type: ignore - # Don't change it, JSON and XML ElementTree are totally able - # to serialize correctly u'' strings - return data - except NameError: - return str(data) - return str(data) - - def serialize_iter(self, data, iter_type, div=None, **kwargs): - """Serialize iterable. - - Supported kwargs: - - serialization_ctxt dict : The current entry of _attribute_map, or same format. - serialization_ctxt['type'] should be same as data_type. - - is_xml bool : If set, serialize as XML - - :param list data: Object to be serialized. - :param str iter_type: Type of object in the iterable. - :param str div: If set, this str will be used to combine the elements - in the iterable into a combined string. Default is 'None'. - Defaults to False. - :rtype: list, str - :return: serialized iterable - """ - if isinstance(data, str): - raise SerializationError("Refuse str type as a valid iter type.") - - serialization_ctxt = kwargs.get("serialization_ctxt", {}) - is_xml = kwargs.get("is_xml", False) - - serialized = [] - for d in data: - try: - serialized.append(self.serialize_data(d, iter_type, **kwargs)) - except ValueError as err: - if isinstance(err, SerializationError): - raise - serialized.append(None) - - if kwargs.get("do_quote", False): - serialized = ["" if s is None else quote(str(s), safe="") for s in serialized] - - if div: - serialized = ["" if s is None else str(s) for s in serialized] - serialized = div.join(serialized) - - if "xml" in serialization_ctxt or is_xml: - # XML serialization is more complicated - xml_desc = serialization_ctxt.get("xml", {}) - xml_name = xml_desc.get("name") - if not xml_name: - xml_name = serialization_ctxt["key"] - - # Create a wrap node if necessary (use the fact that Element and list have "append") - is_wrapped = xml_desc.get("wrapped", False) - node_name = xml_desc.get("itemsName", xml_name) - if is_wrapped: - final_result = _create_xml_node(xml_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) - else: - final_result = [] - # All list elements to "local_node" - for el in serialized: - if isinstance(el, ET.Element): - el_node = el - else: - el_node = _create_xml_node(node_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) - if el is not None: # Otherwise it writes "None" :-p - el_node.text = str(el) - final_result.append(el_node) - return final_result - return serialized - - def serialize_dict(self, attr, dict_type, **kwargs): - """Serialize a dictionary of objects. - - :param dict attr: Object to be serialized. - :param str dict_type: Type of object in the dictionary. - :rtype: dict - :return: serialized dictionary - """ - serialization_ctxt = kwargs.get("serialization_ctxt", {}) - serialized = {} - for key, value in attr.items(): - try: - serialized[self.serialize_unicode(key)] = self.serialize_data(value, dict_type, **kwargs) - except ValueError as err: - if isinstance(err, SerializationError): - raise - serialized[self.serialize_unicode(key)] = None - - if "xml" in serialization_ctxt: - # XML serialization is more complicated - xml_desc = serialization_ctxt["xml"] - xml_name = xml_desc["name"] - - final_result = _create_xml_node(xml_name, xml_desc.get("prefix", None), xml_desc.get("ns", None)) - for key, value in serialized.items(): - ET.SubElement(final_result, key).text = value - return final_result - - return serialized - - def serialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements - """Serialize a generic object. - This will be handled as a dictionary. If object passed in is not - a basic type (str, int, float, dict, list) it will simply be - cast to str. - - :param dict attr: Object to be serialized. - :rtype: dict or str - :return: serialized object - """ - if attr is None: - return None - if isinstance(attr, ET.Element): - return attr - obj_type = type(attr) - if obj_type in self.basic_types: - return self.serialize_basic(attr, self.basic_types[obj_type], **kwargs) - if obj_type is _long_type: - return self.serialize_long(attr) - if obj_type is str: - return self.serialize_unicode(attr) - if obj_type is datetime.datetime: - return self.serialize_iso(attr) - if obj_type is datetime.date: - return self.serialize_date(attr) - if obj_type is datetime.time: - return self.serialize_time(attr) - if obj_type is datetime.timedelta: - return self.serialize_duration(attr) - if obj_type is decimal.Decimal: - return self.serialize_decimal(attr) - - # If it's a model or I know this dependency, serialize as a Model - if obj_type in self.dependencies.values() or isinstance(attr, Model): - return self._serialize(attr) - - if obj_type == dict: - serialized = {} - for key, value in attr.items(): - try: - serialized[self.serialize_unicode(key)] = self.serialize_object(value, **kwargs) - except ValueError: - serialized[self.serialize_unicode(key)] = None - return serialized - - if obj_type == list: - serialized = [] - for obj in attr: - try: - serialized.append(self.serialize_object(obj, **kwargs)) - except ValueError: - pass - return serialized - return str(attr) - - @staticmethod - def serialize_enum(attr, enum_obj=None): - try: - result = attr.value - except AttributeError: - result = attr - try: - enum_obj(result) # type: ignore - return result - except ValueError as exc: - for enum_value in enum_obj: # type: ignore - if enum_value.value.lower() == str(attr).lower(): - return enum_value.value - error = "{!r} is not valid value for enum {!r}" - raise SerializationError(error.format(attr, enum_obj)) from exc - - @staticmethod - def serialize_bytearray(attr, **kwargs): # pylint: disable=unused-argument - """Serialize bytearray into base-64 string. - - :param str attr: Object to be serialized. - :rtype: str - :return: serialized base64 - """ - return b64encode(attr).decode() - - @staticmethod - def serialize_base64(attr, **kwargs): # pylint: disable=unused-argument - """Serialize str into base-64 string. - - :param str attr: Object to be serialized. - :rtype: str - :return: serialized base64 - """ - encoded = b64encode(attr).decode("ascii") - return encoded.strip("=").replace("+", "-").replace("/", "_") - - @staticmethod - def serialize_decimal(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Decimal object to float. - - :param decimal attr: Object to be serialized. - :rtype: float - :return: serialized decimal - """ - return float(attr) - - @staticmethod - def serialize_long(attr, **kwargs): # pylint: disable=unused-argument - """Serialize long (Py2) or int (Py3). - - :param int attr: Object to be serialized. - :rtype: int/long - :return: serialized long - """ - return _long_type(attr) - - @staticmethod - def serialize_date(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Date object into ISO-8601 formatted string. - - :param Date attr: Object to be serialized. - :rtype: str - :return: serialized date - """ - if isinstance(attr, str): - attr = isodate.parse_date(attr) - t = "{:04}-{:02}-{:02}".format(attr.year, attr.month, attr.day) - return t - - @staticmethod - def serialize_time(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Time object into ISO-8601 formatted string. - - :param datetime.time attr: Object to be serialized. - :rtype: str - :return: serialized time - """ - if isinstance(attr, str): - attr = isodate.parse_time(attr) - t = "{:02}:{:02}:{:02}".format(attr.hour, attr.minute, attr.second) - if attr.microsecond: - t += ".{:02}".format(attr.microsecond) - return t - - @staticmethod - def serialize_duration(attr, **kwargs): # pylint: disable=unused-argument - """Serialize TimeDelta object into ISO-8601 formatted string. - - :param TimeDelta attr: Object to be serialized. - :rtype: str - :return: serialized duration - """ - if isinstance(attr, str): - attr = isodate.parse_duration(attr) - return isodate.duration_isoformat(attr) - - @staticmethod - def serialize_rfc(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Datetime object into RFC-1123 formatted string. - - :param Datetime attr: Object to be serialized. - :rtype: str - :raises TypeError: if format invalid. - :return: serialized rfc - """ - try: - if not attr.tzinfo: - _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") - utc = attr.utctimetuple() - except AttributeError as exc: - raise TypeError("RFC1123 object must be valid Datetime object.") from exc - - return "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT".format( - Serializer.days[utc.tm_wday], - utc.tm_mday, - Serializer.months[utc.tm_mon], - utc.tm_year, - utc.tm_hour, - utc.tm_min, - utc.tm_sec, - ) - - @staticmethod - def serialize_iso(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Datetime object into ISO-8601 formatted string. - - :param Datetime attr: Object to be serialized. - :rtype: str - :raises SerializationError: if format invalid. - :return: serialized iso - """ - if isinstance(attr, str): - attr = isodate.parse_datetime(attr) - try: - if not attr.tzinfo: - _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") - utc = attr.utctimetuple() - if utc.tm_year > 9999 or utc.tm_year < 1: - raise OverflowError("Hit max or min date") - - microseconds = str(attr.microsecond).rjust(6, "0").rstrip("0").ljust(3, "0") - if microseconds: - microseconds = "." + microseconds - date = "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}".format( - utc.tm_year, utc.tm_mon, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec - ) - return date + microseconds + "Z" - except (ValueError, OverflowError) as err: - msg = "Unable to serialize datetime object." - raise SerializationError(msg) from err - except AttributeError as err: - msg = "ISO-8601 object must be valid Datetime object." - raise TypeError(msg) from err - - @staticmethod - def serialize_unix(attr, **kwargs): # pylint: disable=unused-argument - """Serialize Datetime object into IntTime format. - This is represented as seconds. - - :param Datetime attr: Object to be serialized. - :rtype: int - :raises SerializationError: if format invalid - :return: serialied unix - """ - if isinstance(attr, int): - return attr - try: - if not attr.tzinfo: - _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") - return int(calendar.timegm(attr.utctimetuple())) - except AttributeError as exc: - raise TypeError("Unix time object must be valid Datetime object.") from exc - - -def rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - key = attr_desc["key"] - working_data = data - - while "." in key: - # Need the cast, as for some reasons "split" is typed as list[str | Any] - dict_keys = cast(List[str], _FLATTEN.split(key)) - if len(dict_keys) == 1: - key = _decode_attribute_map_key(dict_keys[0]) - break - working_key = _decode_attribute_map_key(dict_keys[0]) - working_data = working_data.get(working_key, data) - if working_data is None: - # If at any point while following flatten JSON path see None, it means - # that all properties under are None as well - return None - key = ".".join(dict_keys[1:]) - - return working_data.get(key) - - -def rest_key_case_insensitive_extractor( # pylint: disable=unused-argument, inconsistent-return-statements - attr, attr_desc, data -): - key = attr_desc["key"] - working_data = data - - while "." in key: - dict_keys = _FLATTEN.split(key) - if len(dict_keys) == 1: - key = _decode_attribute_map_key(dict_keys[0]) - break - working_key = _decode_attribute_map_key(dict_keys[0]) - working_data = attribute_key_case_insensitive_extractor(working_key, None, working_data) - if working_data is None: - # If at any point while following flatten JSON path see None, it means - # that all properties under are None as well - return None - key = ".".join(dict_keys[1:]) - - if working_data: - return attribute_key_case_insensitive_extractor(key, None, working_data) - - -def last_rest_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - """Extract the attribute in "data" based on the last part of the JSON path key. - - :param str attr: The attribute to extract - :param dict attr_desc: The attribute description - :param dict data: The data to extract from - :rtype: object - :returns: The extracted attribute - """ - key = attr_desc["key"] - dict_keys = _FLATTEN.split(key) - return attribute_key_extractor(dict_keys[-1], None, data) - - -def last_rest_key_case_insensitive_extractor(attr, attr_desc, data): # pylint: disable=unused-argument - """Extract the attribute in "data" based on the last part of the JSON path key. - - This is the case insensitive version of "last_rest_key_extractor" - :param str attr: The attribute to extract - :param dict attr_desc: The attribute description - :param dict data: The data to extract from - :rtype: object - :returns: The extracted attribute - """ - key = attr_desc["key"] - dict_keys = _FLATTEN.split(key) - return attribute_key_case_insensitive_extractor(dict_keys[-1], None, data) - - -def attribute_key_extractor(attr, _, data): - return data.get(attr) - - -def attribute_key_case_insensitive_extractor(attr, _, data): - found_key = None - lower_attr = attr.lower() - for key in data: - if lower_attr == key.lower(): - found_key = key - break - - return data.get(found_key) - - -def _extract_name_from_internal_type(internal_type): - """Given an internal type XML description, extract correct XML name with namespace. - - :param dict internal_type: An model type - :rtype: tuple - :returns: A tuple XML name + namespace dict - """ - internal_type_xml_map = getattr(internal_type, "_xml_map", {}) - xml_name = internal_type_xml_map.get("name", internal_type.__name__) - xml_ns = internal_type_xml_map.get("ns", None) - if xml_ns: - xml_name = "{{{}}}{}".format(xml_ns, xml_name) - return xml_name - - -def xml_key_extractor(attr, attr_desc, data): # pylint: disable=unused-argument,too-many-return-statements - if isinstance(data, dict): - return None - - # Test if this model is XML ready first - if not isinstance(data, ET.Element): - return None - - xml_desc = attr_desc.get("xml", {}) - xml_name = xml_desc.get("name", attr_desc["key"]) - - # Look for a children - is_iter_type = attr_desc["type"].startswith("[") - is_wrapped = xml_desc.get("wrapped", False) - internal_type = attr_desc.get("internalType", None) - internal_type_xml_map = getattr(internal_type, "_xml_map", {}) - - # Integrate namespace if necessary - xml_ns = xml_desc.get("ns", internal_type_xml_map.get("ns", None)) - if xml_ns: - xml_name = "{{{}}}{}".format(xml_ns, xml_name) - - # If it's an attribute, that's simple - if xml_desc.get("attr", False): - return data.get(xml_name) - - # If it's x-ms-text, that's simple too - if xml_desc.get("text", False): - return data.text - - # Scenario where I take the local name: - # - Wrapped node - # - Internal type is an enum (considered basic types) - # - Internal type has no XML/Name node - if is_wrapped or (internal_type and (issubclass(internal_type, Enum) or "name" not in internal_type_xml_map)): - children = data.findall(xml_name) - # If internal type has a local name and it's not a list, I use that name - elif not is_iter_type and internal_type and "name" in internal_type_xml_map: - xml_name = _extract_name_from_internal_type(internal_type) - children = data.findall(xml_name) - # That's an array - else: - if internal_type: # Complex type, ignore itemsName and use the complex type name - items_name = _extract_name_from_internal_type(internal_type) - else: - items_name = xml_desc.get("itemsName", xml_name) - children = data.findall(items_name) - - if len(children) == 0: - if is_iter_type: - if is_wrapped: - return None # is_wrapped no node, we want None - return [] # not wrapped, assume empty list - return None # Assume it's not there, maybe an optional node. - - # If is_iter_type and not wrapped, return all found children - if is_iter_type: - if not is_wrapped: - return children - # Iter and wrapped, should have found one node only (the wrap one) - if len(children) != 1: - raise DeserializationError( - "Tried to deserialize an array not wrapped, and found several nodes '{}'. Maybe you should declare this array as wrapped?".format( # pylint: disable=line-too-long - xml_name - ) - ) - return list(children[0]) # Might be empty list and that's ok. - - # Here it's not a itertype, we should have found one element only or empty - if len(children) > 1: - raise DeserializationError("Find several XML '{}' where it was not expected".format(xml_name)) - return children[0] - - -class Deserializer: - """Response object model deserializer. - - :param dict classes: Class type dictionary for deserializing complex types. - :ivar list key_extractors: Ordered list of extractors to be used by this deserializer. - """ - - basic_types = {str: "str", int: "int", bool: "bool", float: "float"} - - valid_date = re.compile(r"\d{4}[-]\d{2}[-]\d{2}T\d{2}:\d{2}:\d{2}\.?\d*Z?[-+]?[\d{2}]?:?[\d{2}]?") - - def __init__(self, classes: Optional[Mapping[str, type]] = None) -> None: - self.deserialize_type = { - "iso-8601": Deserializer.deserialize_iso, - "rfc-1123": Deserializer.deserialize_rfc, - "unix-time": Deserializer.deserialize_unix, - "duration": Deserializer.deserialize_duration, - "date": Deserializer.deserialize_date, - "time": Deserializer.deserialize_time, - "decimal": Deserializer.deserialize_decimal, - "long": Deserializer.deserialize_long, - "bytearray": Deserializer.deserialize_bytearray, - "base64": Deserializer.deserialize_base64, - "object": self.deserialize_object, - "[]": self.deserialize_iter, - "{}": self.deserialize_dict, - } - self.deserialize_expected_types = { - "duration": (isodate.Duration, datetime.timedelta), - "iso-8601": (datetime.datetime), - } - self.dependencies: Dict[str, type] = dict(classes) if classes else {} - self.key_extractors = [rest_key_extractor, xml_key_extractor] - # Additional properties only works if the "rest_key_extractor" is used to - # extract the keys. Making it to work whatever the key extractor is too much - # complicated, with no real scenario for now. - # So adding a flag to disable additional properties detection. This flag should be - # used if your expect the deserialization to NOT come from a JSON REST syntax. - # Otherwise, result are unexpected - self.additional_properties_detection = True - - def __call__(self, target_obj, response_data, content_type=None): - """Call the deserializer to process a REST response. - - :param str target_obj: Target data type to deserialize to. - :param requests.Response response_data: REST response object. - :param str content_type: Swagger "produces" if available. - :raises DeserializationError: if deserialization fails. - :return: Deserialized object. - :rtype: object - """ - data = self._unpack_content(response_data, content_type) - return self._deserialize(target_obj, data) - - def _deserialize(self, target_obj, data): # pylint: disable=inconsistent-return-statements - """Call the deserializer on a model. - - Data needs to be already deserialized as JSON or XML ElementTree - - :param str target_obj: Target data type to deserialize to. - :param object data: Object to deserialize. - :raises DeserializationError: if deserialization fails. - :return: Deserialized object. - :rtype: object - """ - # This is already a model, go recursive just in case - if hasattr(data, "_attribute_map"): - constants = [name for name, config in getattr(data, "_validation", {}).items() if config.get("constant")] - try: - for attr, mapconfig in data._attribute_map.items(): # pylint: disable=protected-access - if attr in constants: - continue - value = getattr(data, attr) - if value is None: - continue - local_type = mapconfig["type"] - internal_data_type = local_type.strip("[]{}") - if internal_data_type not in self.dependencies or isinstance(internal_data_type, Enum): - continue - setattr(data, attr, self._deserialize(local_type, value)) - return data - except AttributeError: - return - - response, class_name = self._classify_target(target_obj, data) - - if isinstance(response, str): - return self.deserialize_data(data, response) - if isinstance(response, type) and issubclass(response, Enum): - return self.deserialize_enum(data, response) - - if data is None or data is CoreNull: - return data - try: - attributes = response._attribute_map # type: ignore # pylint: disable=protected-access - d_attrs = {} - for attr, attr_desc in attributes.items(): - # Check empty string. If it's not empty, someone has a real "additionalProperties"... - if attr == "additional_properties" and attr_desc["key"] == "": - continue - raw_value = None - # Enhance attr_desc with some dynamic data - attr_desc = attr_desc.copy() # Do a copy, do not change the real one - internal_data_type = attr_desc["type"].strip("[]{}") - if internal_data_type in self.dependencies: - attr_desc["internalType"] = self.dependencies[internal_data_type] - - for key_extractor in self.key_extractors: - found_value = key_extractor(attr, attr_desc, data) - if found_value is not None: - if raw_value is not None and raw_value != found_value: - msg = ( - "Ignoring extracted value '%s' from %s for key '%s'" - " (duplicate extraction, follow extractors order)" - ) - _LOGGER.warning(msg, found_value, key_extractor, attr) - continue - raw_value = found_value - - value = self.deserialize_data(raw_value, attr_desc["type"]) - d_attrs[attr] = value - except (AttributeError, TypeError, KeyError) as err: - msg = "Unable to deserialize to object: " + class_name # type: ignore - raise DeserializationError(msg) from err - additional_properties = self._build_additional_properties(attributes, data) - return self._instantiate_model(response, d_attrs, additional_properties) - - def _build_additional_properties(self, attribute_map, data): - if not self.additional_properties_detection: - return None - if "additional_properties" in attribute_map and attribute_map.get("additional_properties", {}).get("key") != "": - # Check empty string. If it's not empty, someone has a real "additionalProperties" - return None - if isinstance(data, ET.Element): - data = {el.tag: el.text for el in data} - - known_keys = { - _decode_attribute_map_key(_FLATTEN.split(desc["key"])[0]) - for desc in attribute_map.values() - if desc["key"] != "" - } - present_keys = set(data.keys()) - missing_keys = present_keys - known_keys - return {key: data[key] for key in missing_keys} - - def _classify_target(self, target, data): - """Check to see whether the deserialization target object can - be classified into a subclass. - Once classification has been determined, initialize object. - - :param str target: The target object type to deserialize to. - :param str/dict data: The response data to deserialize. - :return: The classified target object and its class name. - :rtype: tuple - """ - if target is None: - return None, None - - if isinstance(target, str): - try: - target = self.dependencies[target] - except KeyError: - return target, target - - try: - target = target._classify(data, self.dependencies) # type: ignore # pylint: disable=protected-access - except AttributeError: - pass # Target is not a Model, no classify - return target, target.__class__.__name__ # type: ignore - - def failsafe_deserialize(self, target_obj, data, content_type=None): - """Ignores any errors encountered in deserialization, - and falls back to not deserializing the object. Recommended - for use in error deserialization, as we want to return the - HttpResponseError to users, and not have them deal with - a deserialization error. - - :param str target_obj: The target object type to deserialize to. - :param str/dict data: The response data to deserialize. - :param str content_type: Swagger "produces" if available. - :return: Deserialized object. - :rtype: object - """ - try: - return self(target_obj, data, content_type=content_type) - except: # pylint: disable=bare-except - _LOGGER.debug( - "Ran into a deserialization error. Ignoring since this is failsafe deserialization", exc_info=True - ) - return None - - @staticmethod - def _unpack_content(raw_data, content_type=None): - """Extract the correct structure for deserialization. - - If raw_data is a PipelineResponse, try to extract the result of RawDeserializer. - if we can't, raise. Your Pipeline should have a RawDeserializer. - - If not a pipeline response and raw_data is bytes or string, use content-type - to decode it. If no content-type, try JSON. - - If raw_data is something else, bypass all logic and return it directly. - - :param obj raw_data: Data to be processed. - :param str content_type: How to parse if raw_data is a string/bytes. - :raises JSONDecodeError: If JSON is requested and parsing is impossible. - :raises UnicodeDecodeError: If bytes is not UTF8 - :rtype: object - :return: Unpacked content. - """ - # Assume this is enough to detect a Pipeline Response without importing it - context = getattr(raw_data, "context", {}) - if context: - if RawDeserializer.CONTEXT_NAME in context: - return context[RawDeserializer.CONTEXT_NAME] - raise ValueError("This pipeline didn't have the RawDeserializer policy; can't deserialize") - - # Assume this is enough to recognize universal_http.ClientResponse without importing it - if hasattr(raw_data, "body"): - return RawDeserializer.deserialize_from_http_generics(raw_data.text(), raw_data.headers) - - # Assume this enough to recognize requests.Response without importing it. - if hasattr(raw_data, "_content_consumed"): - return RawDeserializer.deserialize_from_http_generics(raw_data.text, raw_data.headers) - - if isinstance(raw_data, (str, bytes)) or hasattr(raw_data, "read"): - return RawDeserializer.deserialize_from_text(raw_data, content_type) # type: ignore - return raw_data - - def _instantiate_model(self, response, attrs, additional_properties=None): - """Instantiate a response model passing in deserialized args. - - :param Response response: The response model class. - :param dict attrs: The deserialized response attributes. - :param dict additional_properties: Additional properties to be set. - :rtype: Response - :return: The instantiated response model. - """ - if callable(response): - subtype = getattr(response, "_subtype_map", {}) - try: - readonly = [ - k - for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore - if v.get("readonly") - ] - const = [ - k - for k, v in response._validation.items() # pylint: disable=protected-access # type: ignore - if v.get("constant") - ] - kwargs = {k: v for k, v in attrs.items() if k not in subtype and k not in readonly + const} - response_obj = response(**kwargs) - for attr in readonly: - setattr(response_obj, attr, attrs.get(attr)) - if additional_properties: - response_obj.additional_properties = additional_properties # type: ignore - return response_obj - except TypeError as err: - msg = "Unable to deserialize {} into model {}. ".format(kwargs, response) # type: ignore - raise DeserializationError(msg + str(err)) from err - else: - try: - for attr, value in attrs.items(): - setattr(response, attr, value) - return response - except Exception as exp: - msg = "Unable to populate response model. " - msg += "Type: {}, Error: {}".format(type(response), exp) - raise DeserializationError(msg) from exp - - def deserialize_data(self, data, data_type): # pylint: disable=too-many-return-statements - """Process data for deserialization according to data type. - - :param str data: The response string to be deserialized. - :param str data_type: The type to deserialize to. - :raises DeserializationError: if deserialization fails. - :return: Deserialized object. - :rtype: object - """ - if data is None: - return data - - try: - if not data_type: - return data - if data_type in self.basic_types.values(): - return self.deserialize_basic(data, data_type) - if data_type in self.deserialize_type: - if isinstance(data, self.deserialize_expected_types.get(data_type, tuple())): - return data - - is_a_text_parsing_type = lambda x: x not in [ # pylint: disable=unnecessary-lambda-assignment - "object", - "[]", - r"{}", - ] - if isinstance(data, ET.Element) and is_a_text_parsing_type(data_type) and not data.text: - return None - data_val = self.deserialize_type[data_type](data) - return data_val - - iter_type = data_type[0] + data_type[-1] - if iter_type in self.deserialize_type: - return self.deserialize_type[iter_type](data, data_type[1:-1]) - - obj_type = self.dependencies[data_type] - if issubclass(obj_type, Enum): - if isinstance(data, ET.Element): - data = data.text - return self.deserialize_enum(data, obj_type) - - except (ValueError, TypeError, AttributeError) as err: - msg = "Unable to deserialize response data." - msg += " Data: {}, {}".format(data, data_type) - raise DeserializationError(msg) from err - return self._deserialize(obj_type, data) - - def deserialize_iter(self, attr, iter_type): - """Deserialize an iterable. - - :param list attr: Iterable to be deserialized. - :param str iter_type: The type of object in the iterable. - :return: Deserialized iterable. - :rtype: list - """ - if attr is None: - return None - if isinstance(attr, ET.Element): # If I receive an element here, get the children - attr = list(attr) - if not isinstance(attr, (list, set)): - raise DeserializationError("Cannot deserialize as [{}] an object of type {}".format(iter_type, type(attr))) - return [self.deserialize_data(a, iter_type) for a in attr] - - def deserialize_dict(self, attr, dict_type): - """Deserialize a dictionary. - - :param dict/list attr: Dictionary to be deserialized. Also accepts - a list of key, value pairs. - :param str dict_type: The object type of the items in the dictionary. - :return: Deserialized dictionary. - :rtype: dict - """ - if isinstance(attr, list): - return {x["key"]: self.deserialize_data(x["value"], dict_type) for x in attr} - - if isinstance(attr, ET.Element): - # Transform value into {"Key": "value"} - attr = {el.tag: el.text for el in attr} - return {k: self.deserialize_data(v, dict_type) for k, v in attr.items()} - - def deserialize_object(self, attr, **kwargs): # pylint: disable=too-many-return-statements - """Deserialize a generic object. - This will be handled as a dictionary. - - :param dict attr: Dictionary to be deserialized. - :return: Deserialized object. - :rtype: dict - :raises TypeError: if non-builtin datatype encountered. - """ - if attr is None: - return None - if isinstance(attr, ET.Element): - # Do no recurse on XML, just return the tree as-is - return attr - if isinstance(attr, str): - return self.deserialize_basic(attr, "str") - obj_type = type(attr) - if obj_type in self.basic_types: - return self.deserialize_basic(attr, self.basic_types[obj_type]) - if obj_type is _long_type: - return self.deserialize_long(attr) - - if obj_type == dict: - deserialized = {} - for key, value in attr.items(): - try: - deserialized[key] = self.deserialize_object(value, **kwargs) - except ValueError: - deserialized[key] = None - return deserialized - - if obj_type == list: - deserialized = [] - for obj in attr: - try: - deserialized.append(self.deserialize_object(obj, **kwargs)) - except ValueError: - pass - return deserialized - - error = "Cannot deserialize generic object with type: " - raise TypeError(error + str(obj_type)) - - def deserialize_basic(self, attr, data_type): # pylint: disable=too-many-return-statements - """Deserialize basic builtin data type from string. - Will attempt to convert to str, int, float and bool. - This function will also accept '1', '0', 'true' and 'false' as - valid bool values. - - :param str attr: response string to be deserialized. - :param str data_type: deserialization data type. - :return: Deserialized basic type. - :rtype: str, int, float or bool - :raises TypeError: if string format is not valid. - """ - # If we're here, data is supposed to be a basic type. - # If it's still an XML node, take the text - if isinstance(attr, ET.Element): - attr = attr.text - if not attr: - if data_type == "str": - # None or '', node is empty string. - return "" - # None or '', node with a strong type is None. - # Don't try to model "empty bool" or "empty int" - return None - - if data_type == "bool": - if attr in [True, False, 1, 0]: - return bool(attr) - if isinstance(attr, str): - if attr.lower() in ["true", "1"]: - return True - if attr.lower() in ["false", "0"]: - return False - raise TypeError("Invalid boolean value: {}".format(attr)) - - if data_type == "str": - return self.deserialize_unicode(attr) - return eval(data_type)(attr) # nosec # pylint: disable=eval-used - - @staticmethod - def deserialize_unicode(data): - """Preserve unicode objects in Python 2, otherwise return data - as a string. - - :param str data: response string to be deserialized. - :return: Deserialized string. - :rtype: str or unicode - """ - # We might be here because we have an enum modeled as string, - # and we try to deserialize a partial dict with enum inside - if isinstance(data, Enum): - return data - - # Consider this is real string - try: - if isinstance(data, unicode): # type: ignore - return data - except NameError: - return str(data) - return str(data) - - @staticmethod - def deserialize_enum(data, enum_obj): - """Deserialize string into enum object. - - If the string is not a valid enum value it will be returned as-is - and a warning will be logged. - - :param str data: Response string to be deserialized. If this value is - None or invalid it will be returned as-is. - :param Enum enum_obj: Enum object to deserialize to. - :return: Deserialized enum object. - :rtype: Enum - """ - if isinstance(data, enum_obj) or data is None: - return data - if isinstance(data, Enum): - data = data.value - if isinstance(data, int): - # Workaround. We might consider remove it in the future. - try: - return list(enum_obj.__members__.values())[data] - except IndexError as exc: - error = "{!r} is not a valid index for enum {!r}" - raise DeserializationError(error.format(data, enum_obj)) from exc - try: - return enum_obj(str(data)) - except ValueError: - for enum_value in enum_obj: - if enum_value.value.lower() == str(data).lower(): - return enum_value - # We don't fail anymore for unknown value, we deserialize as a string - _LOGGER.warning("Deserializer is not able to find %s as valid enum in %s", data, enum_obj) - return Deserializer.deserialize_unicode(data) - - @staticmethod - def deserialize_bytearray(attr): - """Deserialize string into bytearray. - - :param str attr: response string to be deserialized. - :return: Deserialized bytearray - :rtype: bytearray - :raises TypeError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - return bytearray(b64decode(attr)) # type: ignore - - @staticmethod - def deserialize_base64(attr): - """Deserialize base64 encoded string into string. - - :param str attr: response string to be deserialized. - :return: Deserialized base64 string - :rtype: bytearray - :raises TypeError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - padding = "=" * (3 - (len(attr) + 3) % 4) # type: ignore - attr = attr + padding # type: ignore - encoded = attr.replace("-", "+").replace("_", "/") - return b64decode(encoded) - - @staticmethod - def deserialize_decimal(attr): - """Deserialize string into Decimal object. - - :param str attr: response string to be deserialized. - :return: Deserialized decimal - :raises DeserializationError: if string format invalid. - :rtype: decimal - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - return decimal.Decimal(str(attr)) # type: ignore - except decimal.DecimalException as err: - msg = "Invalid decimal {}".format(attr) - raise DeserializationError(msg) from err - - @staticmethod - def deserialize_long(attr): - """Deserialize string into long (Py2) or int (Py3). - - :param str attr: response string to be deserialized. - :return: Deserialized int - :rtype: long or int - :raises ValueError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - return _long_type(attr) # type: ignore - - @staticmethod - def deserialize_duration(attr): - """Deserialize ISO-8601 formatted string into TimeDelta object. - - :param str attr: response string to be deserialized. - :return: Deserialized duration - :rtype: TimeDelta - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - duration = isodate.parse_duration(attr) - except (ValueError, OverflowError, AttributeError) as err: - msg = "Cannot deserialize duration object." - raise DeserializationError(msg) from err - return duration - - @staticmethod - def deserialize_date(attr): - """Deserialize ISO-8601 formatted string into Date object. - - :param str attr: response string to be deserialized. - :return: Deserialized date - :rtype: Date - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - if re.search(r"[^\W\d_]", attr, re.I + re.U): # type: ignore - raise DeserializationError("Date must have only digits and -. Received: %s" % attr) - # This must NOT use defaultmonth/defaultday. Using None ensure this raises an exception. - return isodate.parse_date(attr, defaultmonth=0, defaultday=0) - - @staticmethod - def deserialize_time(attr): - """Deserialize ISO-8601 formatted string into time object. - - :param str attr: response string to be deserialized. - :return: Deserialized time - :rtype: datetime.time - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - if re.search(r"[^\W\d_]", attr, re.I + re.U): # type: ignore - raise DeserializationError("Date must have only digits and -. Received: %s" % attr) - return isodate.parse_time(attr) - - @staticmethod - def deserialize_rfc(attr): - """Deserialize RFC-1123 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :return: Deserialized RFC datetime - :rtype: Datetime - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - parsed_date = email.utils.parsedate_tz(attr) # type: ignore - date_obj = datetime.datetime( - *parsed_date[:6], tzinfo=datetime.timezone(datetime.timedelta(minutes=(parsed_date[9] or 0) / 60)) - ) - if not date_obj.tzinfo: - date_obj = date_obj.astimezone(tz=TZ_UTC) - except ValueError as err: - msg = "Cannot deserialize to rfc datetime object." - raise DeserializationError(msg) from err - return date_obj - - @staticmethod - def deserialize_iso(attr): - """Deserialize ISO-8601 formatted string into Datetime object. - - :param str attr: response string to be deserialized. - :return: Deserialized ISO datetime - :rtype: Datetime - :raises DeserializationError: if string format invalid. - """ - if isinstance(attr, ET.Element): - attr = attr.text - try: - attr = attr.upper() # type: ignore - match = Deserializer.valid_date.match(attr) - if not match: - raise ValueError("Invalid datetime string: " + attr) - - check_decimal = attr.split(".") - if len(check_decimal) > 1: - decimal_str = "" - for digit in check_decimal[1]: - if digit.isdigit(): - decimal_str += digit - else: - break - if len(decimal_str) > 6: - attr = attr.replace(decimal_str, decimal_str[0:6]) - - date_obj = isodate.parse_datetime(attr) - test_utc = date_obj.utctimetuple() - if test_utc.tm_year > 9999 or test_utc.tm_year < 1: - raise OverflowError("Hit max or min date") - except (ValueError, OverflowError, AttributeError) as err: - msg = "Cannot deserialize datetime object." - raise DeserializationError(msg) from err - return date_obj - - @staticmethod - def deserialize_unix(attr): - """Serialize Datetime object into IntTime format. - This is represented as seconds. - - :param int attr: Object to be serialized. - :return: Deserialized datetime - :rtype: Datetime - :raises DeserializationError: if format invalid - """ - if isinstance(attr, ET.Element): - attr = int(attr.text) # type: ignore - try: - attr = int(attr) - date_obj = datetime.datetime.fromtimestamp(attr, TZ_UTC) - except ValueError as err: - msg = "Cannot deserialize to unix datetime object." - raise DeserializationError(msg) from err - return date_obj diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/model_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/model_base.py index db24930fdca9..4102784f9a85 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/model_base.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/model_base.py @@ -23,14 +23,19 @@ from json import JSONEncoder import xml.etree.ElementTree as ET from collections.abc import MutableMapping -from typing_extensions import Self import isodate from azure.core.exceptions import DeserializationError from azure.core import CaseInsensitiveEnumMeta from azure.core.pipeline import PipelineResponse from azure.core.serialization import _Null + from azure.core.rest import HttpResponse +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + _LOGGER = logging.getLogger(__name__) __all__ = ["SdkJSONEncoder", "Model", "rest_field", "rest_discriminator"] @@ -595,11 +600,7 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: class_name = self.__class__.__name__ if len(args) > 1: raise TypeError(f"{class_name}.__init__() takes 2 positional arguments but {len(args) + 1} were given") - dict_to_pass = { - rest_field._rest_name: rest_field._default - for rest_field in self._attr_to_rest_field.values() - if rest_field._default is not _UNSET - } + dict_to_pass: dict[str, typing.Any] = {} if args: if isinstance(args[0], ET.Element): dict_to_pass.update(self._init_from_xml(args[0])) @@ -619,6 +620,14 @@ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: if v is not None } ) + # Apply client default values for fields the caller didn't set so that + # defaults are part of `_data` and therefore included during serialization. + for rf in self._attr_to_rest_field.values(): + if rf._default is _UNSET: + continue + if rf._rest_name in dict_to_pass: + continue + dict_to_pass[rf._rest_name] = _create_value(rf, rf._default) super().__init__(dict_to_pass) def _init_from_xml(self, element: ET.Element) -> dict[str, typing.Any]: @@ -1113,7 +1122,10 @@ def __get__(self, obj: Model, type=None): # pylint: disable=redefined-builtin # by this point, type and rest_name will have a value bc we default # them in __new__ of the Model class # Use _data.get() directly to avoid triggering __getitem__ which clears the cache - item = obj._data.get(self._rest_name) + item = obj._data.get(self._rest_name, _UNSET) + if item is _UNSET: + # Field not set by user; return the client default if one exists, otherwise None + return self._default if self._default is not _UNSET else None if item is None: return item if self._is_model: diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/serialization.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/serialization.py index 81ec1de5922b..954bf7ebffa7 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/serialization.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_utils/serialization.py @@ -39,11 +39,15 @@ import xml.etree.ElementTree as ET import isodate # type: ignore -from typing_extensions import Self from azure.core.exceptions import DeserializationError, SerializationError from azure.core.serialization import NULL as CoreNull +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + _BOM = codecs.BOM_UTF8.decode(encoding="utf-8") JSON = MutableMapping[str, Any] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_vendor.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_vendor.py deleted file mode 100644 index 3790083b97e3..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_vendor.py +++ /dev/null @@ -1,25 +0,0 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - -from abc import ABC -from typing import TYPE_CHECKING - -from ._configuration import KeyVaultClientConfiguration - -if TYPE_CHECKING: - from azure.core import PipelineClient - - from ._serialization import Deserializer, Serializer - - -class KeyVaultClientMixinABC(ABC): - """DO NOT use this class. It is for internal typing use only.""" - - _client: "PipelineClient" - _config: KeyVaultClientConfiguration - _serialize: "Serializer" - _deserialize: "Deserializer" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_version.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_version.py index ecd03f6e9262..c8302cef558b 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_version.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "4.10.0" +VERSION = "4.12.0b1" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_client.py index 11ddf0a7723c..9774eded4af6 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_client.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_client.py @@ -7,8 +7,8 @@ # -------------------------------------------------------------------------- from copy import deepcopy +import sys from typing import Any, Awaitable, TYPE_CHECKING -from typing_extensions import Self from azure.core import AsyncPipelineClient from azure.core.pipeline import policies @@ -18,6 +18,11 @@ from ._configuration import KeyVaultClientConfiguration from ._operations import _KeyVaultClientOperationsMixin +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self # type: ignore + if TYPE_CHECKING: from azure.core.credentials_async import AsyncTokenCredential @@ -30,9 +35,10 @@ class KeyVaultClient(_KeyVaultClientOperationsMixin): :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_configuration.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_configuration.py index 68ac794d5103..b499f09437c0 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_configuration.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_configuration.py @@ -26,14 +26,15 @@ class KeyVaultClientConfiguration: # pylint: disable=too-many-instance-attribut :type vault_base_url: str :param credential: Credential used to authenticate requests to the service. Required. :type credential: ~azure.core.credentials_async.AsyncTokenCredential - :keyword api_version: The API version to use for this operation. Known values are "2025-07-01". - Default value is "2025-07-01". Note that overriding this default value may result in - unsupported behavior. + :keyword api_version: The API version to use for this operation. Known values are + "2026-03-01-preview" and None. Default value is None. If not set, the operation's default API + version will be used. Note that overriding this default value may result in unsupported + behavior. :paramtype api_version: str """ def __init__(self, vault_base_url: str, credential: "AsyncTokenCredential", **kwargs: Any) -> None: - api_version: str = kwargs.pop("api_version", "2025-07-01") + api_version: str = kwargs.pop("api_version", "2026-03-01-preview") if vault_base_url is None: raise ValueError("Parameter 'vault_base_url' must not be None.") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_operations.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_operations.py index 88a1892f9ef2..88300c730764 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_operations.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_operations.py @@ -51,6 +51,8 @@ build_key_vault_release_request, build_key_vault_restore_key_request, build_key_vault_rotate_key_request, + build_key_vault_secure_unwrap_key_request, + build_key_vault_secure_wrap_key_request, build_key_vault_sign_request, build_key_vault_unwrap_key_request, build_key_vault_update_key_request, @@ -878,7 +880,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -976,7 +981,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -2256,6 +2264,428 @@ async def wrap_key( return deserialized # type: ignore + @overload + async def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: _models.SecureKeyWrapOperationParameters, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyWrapOperationParameters + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Required. + :type parameters: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Required. + :type parameters: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-01-01-preview", + params_added_on={"2026-01-01-preview": ["key_name", "key_version", "content_type", "accept", "api_version"]}, + api_versions_list=["2026-01-01-preview", "2026-03-01-preview"], + ) + async def secure_wrap_key( + self, + key_name: str, + key_version: str, + parameters: Union[_models.SecureKeyWrapOperationParameters, JSON, IO[bytes]], + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Creates a new 256 bit AES key within the trusted execution environment and wraps this key using + a specified key. + + The SECURE WRAP operation creates a new 256 bit AES key within the trusted execution + environment(TEE) and encrypts the same with a key encryption key that has previously been + stored in an Azure Key Vault. The WRAP operation is only strictly necessary for symmetric keys + stored in Azure Key Vault since protection with an asymmetric key can be performed using the + public portion of the key. This operation is supported for asymmetric keys as a convenience for + callers that have a key-reference but do not have access to the public key material. This + operation requires the keys/wrapKey permission. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for wrap operation. Is one of the following types: + SecureKeyWrapOperationParameters, JSON, IO[bytes] Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyWrapOperationParameters or + JSON or IO[bytes] + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.SecureKeyOperationResult] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(parameters, (IOBase, bytes)): + _content = parameters + else: + _content = json.dumps(parameters, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_key_vault_secure_wrap_key_request( + key_name=key_name, + key_version=key_version, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "vaultBaseUrl": self._serialize.url( + "self._config.vault_base_url", self._config.vault_base_url, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.SecureKeyOperationResult, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + + @overload + async def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: _models.SecureKeyUnWrapOperationParameters, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyUnWrapOperationParameters + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Required. + :type parameters: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @overload + async def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Required. + :type parameters: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + + @distributed_trace_async + @api_version_validation( + method_added_on="2026-01-01-preview", + params_added_on={"2026-01-01-preview": ["key_name", "key_version", "content_type", "accept", "api_version"]}, + api_versions_list=["2026-01-01-preview", "2026-03-01-preview"], + ) + async def secure_unwrap_key( + self, + key_name: str, + key_version: str, + parameters: Union[_models.SecureKeyUnWrapOperationParameters, JSON, IO[bytes]], + **kwargs: Any + ) -> _models.SecureKeyOperationResult: + """Securely unwraps a previously wrapped symmetric key using a specified key, ensuring TEE + attestation via Microsoft Azure Attestation (MAA) before unwrapping. + + The SECURE UNWRAP operation supports decryption of a symmetric key using the target key + encryption key. This operation is the reverse of the SECURE WRAP operation. The SECURE UNWRAP + operation applies to asymmetric and symmetric keys stored in Azure Key Vault since it uses the + private portion of the key. This operation requires the keys/unwrapKey permission. The SECURE + UNWRAP operation ensures that MAA (Microsoft Azure Attestation Service) is used to attest the + TEE (Trusted Execution Environment) before the key is unwrapped. + + :param key_name: The name of the key. Required. + :type key_name: str + :param key_version: The version of the key. Required. + :type key_version: str + :param parameters: The parameters for unwrap operation. Is one of the following types: + SecureKeyUnWrapOperationParameters, JSON, IO[bytes] Required. + :type parameters: ~azure.keyvault.keys._generated.models.SecureKeyUnWrapOperationParameters or + JSON or IO[bytes] + :return: SecureKeyOperationResult. The SecureKeyOperationResult is compatible with + MutableMapping + :rtype: ~azure.keyvault.keys._generated.models.SecureKeyOperationResult + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map: MutableMapping = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop("content_type", _headers.pop("Content-Type", None)) + cls: ClsType[_models.SecureKeyOperationResult] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _content = None + if isinstance(parameters, (IOBase, bytes)): + _content = parameters + else: + _content = json.dumps(parameters, cls=SdkJSONEncoder, exclude_readonly=True) # type: ignore + + _request = build_key_vault_secure_unwrap_key_request( + key_name=key_name, + key_version=key_version, + content_type=content_type, + api_version=self._config.api_version, + content=_content, + headers=_headers, + params=_params, + ) + path_format_arguments = { + "vaultBaseUrl": self._serialize.url( + "self._config.vault_base_url", self._config.vault_base_url, "str", skip_quote=True + ), + } + _request.url = self._client.format_url(_request.url, **path_format_arguments) + + _decompress = kwargs.pop("decompress", True) + _stream = kwargs.pop("stream", False) + pipeline_response: PipelineResponse = await self._client._pipeline.run( # type: ignore # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + try: + await response.read() # Load the body in memory and close the socket + except (StreamConsumedError, StreamClosedError): + pass + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = _failsafe_deserialize( + _models.KeyVaultError, + response, + ) + raise HttpResponseError(response=response, model=error) + + if _stream: + deserialized = response.iter_bytes() if _decompress else response.iter_raw() + else: + deserialized = _deserialize(_models.SecureKeyOperationResult, response.json()) + + if cls: + return cls(pipeline_response, deserialized, {}) # type: ignore + + return deserialized # type: ignore + @overload async def unwrap_key( self, @@ -2688,7 +3118,10 @@ def prepare_request(next_link=None): ) _next_request_params["api-version"] = self._config.api_version _request = HttpRequest( - "GET", urllib.parse.urljoin(next_link, _parsed_next_link.path), params=_next_request_params + "GET", + urllib.parse.urljoin(next_link, _parsed_next_link.path), + headers=_headers, + params=_next_request_params, ) path_format_arguments = { "vaultBaseUrl": self._serialize.url( @@ -3295,7 +3728,14 @@ async def get_random_bytes( @api_version_validation( method_added_on="7.6-preview.2", params_added_on={"7.6-preview.2": ["key_name", "key_version", "accept", "api_version"]}, - api_versions_list=["7.6-preview.2", "7.6", "2025-06-01-preview", "2025-07-01"], + api_versions_list=[ + "7.6-preview.2", + "7.6", + "2025-06-01-preview", + "2025-07-01", + "2026-01-01-preview", + "2026-03-01-preview", + ], ) async def get_key_attestation(self, key_name: str, key_version: str, **kwargs: Any) -> _models.KeyBundle: """Gets the public part of a stored key along with its attestation blob. diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_patch.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_patch.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_operations/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_patch.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_patch.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_vendor.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_vendor.py deleted file mode 100644 index 2b1f525d61ea..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/aio/_vendor.py +++ /dev/null @@ -1,25 +0,0 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - -from abc import ABC -from typing import TYPE_CHECKING - -from ._configuration import KeyVaultClientConfiguration - -if TYPE_CHECKING: - from azure.core import AsyncPipelineClient - - from .._serialization import Deserializer, Serializer - - -class KeyVaultClientMixinABC(ABC): - """DO NOT use this class. It is for internal typing use only.""" - - _client: "AsyncPipelineClient" - _config: KeyVaultClientConfiguration - _serialize: "Serializer" - _deserialize: "Deserializer" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/__init__.py index 9cc503f26c60..1374bc47baad 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/__init__.py @@ -17,6 +17,7 @@ BackupKeyResult, DeletedKeyBundle, DeletedKeyItem, + ExternalKey, GetRandomBytesRequest, JsonWebKey, KeyAttestation, @@ -43,6 +44,9 @@ LifetimeActionsTrigger, LifetimeActionsType, RandomBytes, + SecureKeyOperationResult, + SecureKeyUnWrapOperationParameters, + SecureKeyWrapOperationParameters, ) from ._enums import ( # type: ignore @@ -52,6 +56,7 @@ JsonWebKeyOperation, JsonWebKeySignatureAlgorithm, JsonWebKeyType, + JsonWebKeyWrapAlgorithm, KeyEncryptionAlgorithm, KeyRotationPolicyAction, ) @@ -63,6 +68,7 @@ "BackupKeyResult", "DeletedKeyBundle", "DeletedKeyItem", + "ExternalKey", "GetRandomBytesRequest", "JsonWebKey", "KeyAttestation", @@ -89,12 +95,16 @@ "LifetimeActionsTrigger", "LifetimeActionsType", "RandomBytes", + "SecureKeyOperationResult", + "SecureKeyUnWrapOperationParameters", + "SecureKeyWrapOperationParameters", "DeletionRecoveryLevel", "JsonWebKeyCurveName", "JsonWebKeyEncryptionAlgorithm", "JsonWebKeyOperation", "JsonWebKeySignatureAlgorithm", "JsonWebKeyType", + "JsonWebKeyWrapAlgorithm", "KeyEncryptionAlgorithm", "KeyRotationPolicyAction", ] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_enums.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_enums.py index e6ef90414b95..d7224654b658 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_enums.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_enums.py @@ -206,14 +206,38 @@ class JsonWebKeyType(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Octet sequence (used to represent symmetric keys) which is stored the HSM.""" +class JsonWebKeyWrapAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """An algorithm used for key wrapping and unwrapping.""" + + RSA_OAEP256 = "RSA-OAEP-256" + """RSAES using Optimal Asymmetric Encryption Padding with a hash function of SHA-256 and a mask + generation function of MGF1 with SHA-256.""" + A128_KW = "A128KW" + """128-bit AES key wrap.""" + A192_KW = "A192KW" + """192-bit AES key wrap.""" + A256_KW = "A256KW" + """256-bit AES key wrap.""" + A256_KWPAD = "A256KWPAD" + """128-bit AES key wrap with padding.""" + A128_KWPAD = "A128KWPAD" + """192-bit AES key wrap with padding.""" + A192_KWPAD = "A192KWPAD" + """256-bit AES key wrap with padding.""" + CKM_AES_KEY_WRAP = "CKM_AES_KEY_WRAP" + """CKM AES key wrap.""" + CKM_AES_KEY_WRAP_PAD = "CKM_AES_KEY_WRAP_PAD" + """CKM AES key wrap with padding.""" + + class KeyEncryptionAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): """The encryption algorithm to use to protected the exported key material.""" CKM_RSA_AES_KEY_WRAP = "CKM_RSA_AES_KEY_WRAP" """The CKM_RSA_AES_KEY_WRAP key wrap mechanism.""" - RSA_AES_KEY_WRAP_256 = "RSA_AES_KEY_WRAP_256" + RSA_AES_KEY_WRAP256 = "RSA_AES_KEY_WRAP_256" """The RSA_AES_KEY_WRAP_256 key wrap mechanism.""" - RSA_AES_KEY_WRAP_384 = "RSA_AES_KEY_WRAP_384" + RSA_AES_KEY_WRAP384 = "RSA_AES_KEY_WRAP_384" """The RSA_AES_KEY_WRAP_384 key wrap mechanism.""" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_models.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_models.py index 0517ee3a6f6d..7328b5a156cc 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_models.py @@ -167,6 +167,36 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) +class ExternalKey(_Model): + """External Key parameters. + + :ivar id: The external key identifier. The valid id can only contain characters in the set + [a-zA-Z0-9-]. Maximum length is 64 characters. Required. + :vartype id: str + """ + + id: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """The external key identifier. The valid id can only contain characters in the set [a-zA-Z0-9-]. + Maximum length is 64 characters. Required.""" + + @overload + def __init__( + self, + *, + id: str, # pylint: disable=redefined-builtin + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + class GetRandomBytesRequest(_Model): """The get random bytes request object. @@ -399,6 +429,11 @@ class KeyAttributes(_Model): :vartype hsm_platform: str :ivar attestation: The key or key version attestation information. :vartype attestation: ~azure.keyvault.keys._generated.models.KeyAttestation + :ivar external_key: The external key information. + :vartype external_key: ~azure.keyvault.keys._generated.models.ExternalKey + :ivar key_size: The optional key size in bits for symmetric keys. For example: 128, 192, or 256 + for AES keys. + :vartype key_size: int """ enabled: Optional[bool] = rest_field(visibility=["read", "create", "update", "delete", "query"]) @@ -434,6 +469,12 @@ class KeyAttributes(_Model): """The underlying HSM Platform.""" attestation: Optional["_models.KeyAttestation"] = rest_field(visibility=["read"]) """The key or key version attestation information.""" + external_key: Optional["_models.ExternalKey"] = rest_field( + visibility=["read", "create", "update", "delete", "query"] + ) + """The external key information.""" + key_size: Optional[int] = rest_field(visibility=["read"]) + """The optional key size in bits for symmetric keys. For example: 128, 192, or 256 for AES keys.""" @overload def __init__( @@ -443,6 +484,7 @@ def __init__( not_before: Optional[datetime.datetime] = None, expires: Optional[datetime.datetime] = None, exportable: Optional[bool] = None, + external_key: Optional["_models.ExternalKey"] = None, ) -> None: ... @overload @@ -1310,3 +1352,128 @@ def __init__(self, mapping: Mapping[str, Any]) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) + + +class SecureKeyOperationResult(_Model): + """The secure key wrap operation result. + + :ivar kid: Key identifier. Required. + :vartype kid: str + :ivar algorithm: The algorithm used for the operation. Required. Known values are: + "RSA-OAEP-256", "A128KW", "A192KW", "A256KW", "A256KWPAD", "A128KWPAD", "A192KWPAD", + "CKM_AES_KEY_WRAP", and "CKM_AES_KEY_WRAP_PAD". + :vartype algorithm: str or ~azure.keyvault.keys._generated.models.JsonWebKeyWrapAlgorithm + :ivar value: The result of the operation. Required. + :vartype value: bytes + """ + + kid: str = rest_field(visibility=["read", "create", "update", "delete", "query"]) + """Key identifier. Required.""" + algorithm: Union[str, "_models.JsonWebKeyWrapAlgorithm"] = rest_field( + name="alg", visibility=["read", "create", "update", "delete", "query"] + ) + """The algorithm used for the operation. Required. Known values are: \"RSA-OAEP-256\", \"A128KW\", + \"A192KW\", \"A256KW\", \"A256KWPAD\", \"A128KWPAD\", \"A192KWPAD\", \"CKM_AES_KEY_WRAP\", and + \"CKM_AES_KEY_WRAP_PAD\".""" + value: bytes = rest_field(visibility=["read", "create", "update", "delete", "query"], format="base64url") + """The result of the operation. Required.""" + + @overload + def __init__( + self, + *, + kid: str, + algorithm: Union[str, "_models.JsonWebKeyWrapAlgorithm"], + value: bytes, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class SecureKeyUnWrapOperationParameters(_Model): + """The Secure Key unwrap attributes. + + :ivar algorithm: algorithm identifier. Required. Known values are: "RSA-OAEP-256", "A128KW", + "A192KW", "A256KW", "A256KWPAD", "A128KWPAD", "A192KWPAD", "CKM_AES_KEY_WRAP", and + "CKM_AES_KEY_WRAP_PAD". + :vartype algorithm: str or ~azure.keyvault.keys._generated.models.JsonWebKeyWrapAlgorithm + :ivar value: The value to operate on. Required. + :vartype value: bytes + :ivar target_attestation_token: The attestation assertion for the target of the key release. + Required. + :vartype target_attestation_token: str + """ + + algorithm: Union[str, "_models.JsonWebKeyWrapAlgorithm"] = rest_field( + name="alg", visibility=["read", "create", "update", "delete", "query"] + ) + """algorithm identifier. Required. Known values are: \"RSA-OAEP-256\", \"A128KW\", \"A192KW\", + \"A256KW\", \"A256KWPAD\", \"A128KWPAD\", \"A192KWPAD\", \"CKM_AES_KEY_WRAP\", and + \"CKM_AES_KEY_WRAP_PAD\".""" + value: bytes = rest_field(visibility=["read", "create", "update", "delete", "query"], format="base64url") + """The value to operate on. Required.""" + target_attestation_token: str = rest_field( + name="target", visibility=["read", "create", "update", "delete", "query"] + ) + """The attestation assertion for the target of the key release. Required.""" + + @overload + def __init__( + self, + *, + algorithm: Union[str, "_models.JsonWebKeyWrapAlgorithm"], + value: bytes, + target_attestation_token: str, + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + +class SecureKeyWrapOperationParameters(_Model): + """The Secure Key wrap attributes. + + :ivar algorithm: algorithm identifier. Required. Known values are: "RSA-OAEP-256", "A128KW", + "A192KW", "A256KW", "A256KWPAD", "A128KWPAD", "A192KWPAD", "CKM_AES_KEY_WRAP", and + "CKM_AES_KEY_WRAP_PAD". + :vartype algorithm: str or ~azure.keyvault.keys._generated.models.JsonWebKeyWrapAlgorithm + """ + + algorithm: Union[str, "_models.JsonWebKeyWrapAlgorithm"] = rest_field( + name="alg", visibility=["read", "create", "update", "delete", "query"] + ) + """algorithm identifier. Required. Known values are: \"RSA-OAEP-256\", \"A128KW\", \"A192KW\", + \"A256KW\", \"A256KWPAD\", \"A128KWPAD\", \"A192KWPAD\", \"CKM_AES_KEY_WRAP\", and + \"CKM_AES_KEY_WRAP_PAD\".""" + + @overload + def __init__( + self, + *, + algorithm: Union[str, "_models.JsonWebKeyWrapAlgorithm"], + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: + """ + :param mapping: raw JSON to initialize the model. + :type mapping: Mapping[str, Any] + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_patch.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_patch.py index f7dd32510333..ea765788358a 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_patch.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_generated/models/_patch.py @@ -1,14 +1,14 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ +# coding=utf-8 +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------- """Customize generated code here. Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ -from typing import List -__all__: List[str] = [] # Add all objects you want publicly available to users at this package level +__all__: list[str] = [] # Add all objects you want publicly available to users at this package level def patch_sdk(): diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py deleted file mode 100644 index fb4255c78129..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py +++ /dev/null @@ -1,659 +0,0 @@ -# pylint: disable=line-too-long,useless-suppression -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------- -from collections import namedtuple -from datetime import datetime -from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING - -from ._enums import KeyOperation, KeyRotationPolicyAction, KeyType -from ._shared import parse_key_vault_id -from ._generated.models import JsonWebKey as _JsonWebKey - -if TYPE_CHECKING: - from ._generated import models as _models - -KeyOperationResult = namedtuple("KeyOperationResult", ["id", "value"]) - - -class JsonWebKey(object): - """As defined in http://tools.ietf.org/html/draft-ietf-jose-json-web-key-18. All parameters are optional. - - :keyword str kid: Key identifier. - :keyword kty: Key Type (kty), as defined in https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 - :paramtype kty: ~azure.keyvault.keys.KeyType or str - :keyword key_ops: Allowed operations for the key - :paramtype key_ops: list[str or ~azure.keyvault.keys.KeyOperation] - :keyword bytes n: RSA modulus. - :keyword bytes e: RSA public exponent. - :keyword bytes d: RSA private exponent, or the D component of an EC private key. - :keyword bytes dp: RSA private key parameter. - :keyword bytes dq: RSA private key parameter. - :keyword bytes qi: RSA private key parameter. - :keyword bytes p: RSA secret prime. - :keyword bytes q: RSA secret prime, with p < q. - :keyword bytes k: Symmetric key. - :keyword bytes t: HSM Token, used with 'Bring Your Own Key'. - :keyword crv: Elliptic curve name. - :paramtype crv: ~azure.keyvault.keys.KeyCurveName or str - :keyword bytes x: X component of an EC public key. - :keyword bytes y: Y component of an EC public key. - """ - - _FIELDS = ("kid", "kty", "key_ops", "n", "e", "d", "dp", "dq", "qi", "p", "q", "k", "t", "crv", "x", "y") - - def __init__(self, **kwargs: Any) -> None: - for field in self._FIELDS: - setattr(self, field, kwargs.get(field)) - - def _to_generated_model(self) -> _JsonWebKey: - jwk = _JsonWebKey() - for field in self._FIELDS: - setattr(jwk, field, getattr(self, field)) - return jwk - - -class KeyAttestation: - """The key attestation information. - - :ivar certificate_pem_file: The certificate used for attestation validation, in PEM format. - :vartype certificate_pem_file: bytes or None - :ivar private_key_attestation: The key attestation corresponding to the private key material of the key. - :vartype private_key_attestation: bytes or None - :ivar public_key_attestation: The key attestation corresponding to the public key material of the key. - :vartype public_key_attestation: bytes or None - :ivar version: The version of the attestation. - :vartype version: str or None - """ - - def __init__( - self, - *, - certificate_pem_file: Optional[bytes] = None, - private_key_attestation: Optional[bytes] = None, - public_key_attestation: Optional[bytes] = None, - version: Optional[str] = None, - ) -> None: - self.certificate_pem_file = certificate_pem_file - self.private_key_attestation = private_key_attestation - self.public_key_attestation = public_key_attestation - self.version = version - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_generated(cls, attestation: "_models.KeyAttestation") -> "KeyAttestation": - return cls( - certificate_pem_file=attestation.certificate_pem_file, - private_key_attestation=attestation.private_key_attestation, - public_key_attestation=attestation.public_key_attestation, - version=attestation.version, - ) - - -class KeyProperties(object): - """A key's ID and attributes. - - :param str key_id: The key ID. - :param attributes: The key attributes. - :type attributes: ~azure.keyvault.keys._generated.models.KeyAttributes - - :keyword bool managed: Whether the key's lifetime is managed by Key Vault. - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword release_policy: The azure.keyvault.keys.KeyReleasePolicy specifying the rules under which the key - can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - """ - - def __init__(self, key_id: str, attributes: "Optional[_models.KeyAttributes]" = None, **kwargs: Any) -> None: - self._attributes = attributes - self._id = key_id - self._vault_id = KeyVaultKeyIdentifier(key_id) - self._managed = kwargs.get("managed", None) - self._tags = kwargs.get("tags", None) - self._release_policy = kwargs.pop("release_policy", None) - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_key_bundle(cls, key_bundle: Union["_models.KeyBundle", "_models.DeletedKeyBundle"]) -> "KeyProperties": - # pylint:disable=line-too-long - # release_policy was added in 7.3-preview - release_policy = None - if ( - hasattr(key_bundle, "release_policy") and key_bundle.release_policy is not None # type: ignore[attr-defined] - ): - release_policy = KeyReleasePolicy( - encoded_policy=key_bundle.release_policy.encoded_policy, # type: ignore - content_type=key_bundle.release_policy.content_type, # type: ignore[attr-defined] - immutable=key_bundle.release_policy.immutable, # type: ignore[attr-defined] - ) - - return cls( - key_bundle.key.kid, # type: ignore - attributes=key_bundle.attributes, - managed=key_bundle.managed, - tags=key_bundle.tags, - release_policy=release_policy, - ) - - @classmethod - def _from_key_item(cls, key_item: Union["_models.KeyItem", "_models.DeletedKeyItem"]) -> "KeyProperties": - return cls( - key_id=key_item.kid, # type: ignore - attributes=key_item.attributes, - managed=key_item.managed, - tags=key_item.tags, - ) - - @property - def id(self) -> str: - """The key ID. - - :returns: The key ID. - :rtype: str - """ - return self._id - - @property - def name(self) -> str: - """The key name. - - :returns: The key name. - :rtype: str - """ - return self._vault_id.name - - @property - def version(self) -> Optional[str]: - """The key version. - - :returns: The key version. - :rtype: str or None - """ - return self._vault_id.version - - @property - def enabled(self) -> Optional[bool]: - """Whether the key is enabled for use. - - :returns: True if the key is enabled for use; False otherwise. - :rtype: bool or None - """ - return self._attributes.enabled if self._attributes else None - - @property - def not_before(self) -> Optional[datetime]: - """The time before which the key can not be used, in UTC. - - :returns: The time before which the key can not be used, in UTC. - :rtype: ~datetime.datetime or None - """ - return self._attributes.not_before if self._attributes else None - - @property - def expires_on(self) -> Optional[datetime]: - """When the key will expire, in UTC. - - :returns: When the key will expire, in UTC. - :rtype: ~datetime.datetime or None - """ - return self._attributes.expires if self._attributes else None - - @property - def created_on(self) -> Optional[datetime]: - """When the key was created, in UTC. - - :returns: When the key was created, in UTC. - :rtype: ~datetime.datetime or None - """ - return self._attributes.created if self._attributes else None - - @property - def updated_on(self) -> Optional[datetime]: - """When the key was last updated, in UTC. - - :returns: When the key was last updated, in UTC. - :rtype: ~datetime.datetime or None - """ - return self._attributes.updated if self._attributes else None - - @property - def vault_url(self) -> str: - """URL of the vault containing the key. - - :returns: URL of the vault containing the key. - :rtype: str - """ - return self._vault_id.vault_url - - @property - def recoverable_days(self) -> Optional[int]: - """The number of days the key is retained before being deleted from a soft-delete enabled Key Vault. - - :returns: The number of days the key is retained before being deleted from a soft-delete enabled Key Vault. - :rtype: int or None - """ - # recoverable_days was added in 7.1-preview - if self._attributes: - return getattr(self._attributes, "recoverable_days", None) - return None - - @property - def recovery_level(self) -> Optional[str]: - """The vault's deletion recovery level for keys. - - :returns: The vault's deletion recovery level for keys. - :rtype: str or None - """ - return self._attributes.recovery_level if self._attributes else None - - @property - def tags(self) -> Optional[Dict[str, str]]: - """Application specific metadata in the form of key-value pairs. - - :returns: A dictionary of tags attached to the key. - :rtype: dict[str, str] or None - """ - return self._tags - - @property - def managed(self) -> Optional[bool]: - """Whether the key's lifetime is managed by Key Vault. If the key backs a certificate, this will be true. - - :returns: True if the key's lifetime is managed by Key Vault; False otherwise. - :rtype: bool or None - """ - return self._managed - - @property - def exportable(self) -> Optional[bool]: - """Whether the private key can be exported. - - :returns: True if the private key can be exported; False otherwise. - :rtype: bool or None - """ - # exportable was added in 7.3-preview - if self._attributes: - return getattr(self._attributes, "exportable", None) - return None - - @property - def release_policy(self) -> "Optional[KeyReleasePolicy]": - """The :class:`~azure.keyvault.keys.KeyReleasePolicy` specifying the rules under which the key can be exported. - - :returns: The key's release policy specifying the rules for exporting. - :rtype: ~azure.keyvault.keys.KeyReleasePolicy or None - """ - return self._release_policy - - @property - def hsm_platform(self) -> Optional[str]: - """The underlying HSM platform. - - :returns: The underlying HSM platform. - :rtype: str or None - """ - # hsm_platform was added in 7.5-preview.1 - if self._attributes: - return getattr(self._attributes, "hsm_platform", None) - return None - - @property - def attestation(self) -> Optional[KeyAttestation]: - """The key attestation, if available and requested. - - :returns: The key or key version attestation information. - :rtype: ~azure.keyvault.keys.KeyAttestation or None - """ - # attestation was added in 7.6-preview.2 - if self._attributes: - attestation = getattr(self._attributes, "attestation", None) - if attestation: - return KeyAttestation._from_generated(attestation=attestation) # pylint:disable=protected-access - return None - - -class KeyReleasePolicy(object): - """The policy rules under which a key can be exported. - - :param bytes encoded_policy: The policy rules under which the key can be released. Encoded based on the - ``content_type``. For more information regarding release policy grammar, please refer to: - https://aka.ms/policygrammarkeys for Azure Key Vault; https://aka.ms/policygrammarmhsm for Azure Managed HSM. - - :keyword str content_type: Content type and version of the release policy. Defaults to "application/json; - charset=utf-8" if omitted. - :keyword bool immutable: Marks a release policy as immutable. An immutable release policy cannot be changed or - updated after being marked immutable. Release policies are mutable by default. - """ - - def __init__(self, encoded_policy: bytes, **kwargs: Any) -> None: - self.encoded_policy = encoded_policy - self.content_type = kwargs.get("content_type", None) - self.immutable = kwargs.get("immutable", None) - - -class ReleaseKeyResult(object): - """The result of a key release operation. - - :ivar str value: A signed token containing the released key. - - :param str value: A signed token containing the released key. - """ - - def __init__(self, value: str) -> None: - self.value = value - - -class KeyRotationLifetimeAction(object): - """An action and its corresponding trigger that will be performed by Key Vault over the lifetime of a key. - - :param action: The action that will be executed. - :type action: ~azure.keyvault.keys.KeyRotationPolicyAction or str - - :keyword time_after_create: Time after creation to attempt the specified action, as an ISO 8601 duration. - For example, 90 days is "P90D". See `Wikipedia `_ for more - information on ISO 8601 durations. - :paramtype time_after_create: str or None - :keyword time_before_expiry: Time before expiry to attempt the specified action, as an ISO 8601 duration. - For example, 90 days is "P90D". See `Wikipedia `_ for more - information on ISO 8601 durations. - :paramtype time_before_expiry: str or None - """ - - def __init__(self, action: Union[KeyRotationPolicyAction, str], **kwargs: Any) -> None: - self.action = action - self.time_after_create: Optional[str] = kwargs.get("time_after_create", None) - self.time_before_expiry: Optional[str] = kwargs.get("time_before_expiry", None) - - @classmethod - def _from_generated(cls, lifetime_action: "_models.LifetimeActions") -> "KeyRotationLifetimeAction": - if lifetime_action.action: - if lifetime_action.trigger: - return cls( - action=lifetime_action.action.type, # type: ignore - time_after_create=lifetime_action.trigger.time_after_create, - time_before_expiry=lifetime_action.trigger.time_before_expiry, - ) - return cls(action=lifetime_action.action) # type: ignore - raise ValueError("Provided LifetimeActions model is missing a required lifetime action property.") - - -class KeyRotationPolicy(object): - """The key rotation policy that belongs to a key. - - :ivar id: The identifier of the key rotation policy. - :vartype id: str or None - :ivar lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key. - :vartype lifetime_actions: list[~azure.keyvault.keys.KeyRotationLifetimeAction] - :ivar expires_in: The expiry time of the policy that will be applied on new key versions, defined as an ISO 8601 - duration. For example, 90 days is "P90D". See `Wikipedia `_ for - more information on ISO 8601 durations. - :vartype expires_in: str or None - :ivar created_on: When the policy was created, in UTC - :vartype created_on: ~datetime.datetime or None - :ivar updated_on: When the policy was last updated, in UTC - :vartype updated_on: ~datetime.datetime or None - """ - - def __init__(self, **kwargs: Any) -> None: - self.id = kwargs.get("policy_id", None) - self.lifetime_actions: List[KeyRotationLifetimeAction] = kwargs.get("lifetime_actions", []) - self.expires_in = kwargs.get("expires_in", None) - self.created_on = kwargs.get("created_on", None) - self.updated_on = kwargs.get("updated_on", None) - - @classmethod - def _from_generated(cls, policy: "_models.KeyRotationPolicy") -> "KeyRotationPolicy": - lifetime_actions = ( - [] - if policy.lifetime_actions is None - else [ - KeyRotationLifetimeAction._from_generated(action) # pylint:disable=protected-access - for action in policy.lifetime_actions - ] - ) - if policy.attributes: - return cls( - policy_id=policy.id, - lifetime_actions=lifetime_actions, - expires_in=policy.attributes.expiry_time, - created_on=policy.attributes.created, - updated_on=policy.attributes.updated, - ) - return cls(policy_id=policy.id, lifetime_actions=lifetime_actions) - - -class KeyVaultKey(object): - """A key's attributes and cryptographic material. - - :param str key_id: Key Vault's identifier for the key. Typically a URI, e.g. - https://myvault.vault.azure.net/keys/my-key/version - :param jwk: The key's cryptographic material as a JSON Web Key (https://tools.ietf.org/html/rfc7517). This may be - provided as a dictionary or keyword arguments. See :class:`~azure.keyvault.keys.models.JsonWebKey` for field - names. - :type jwk: Dict[str, Any] - - Providing cryptographic material as keyword arguments: - - .. code-block:: python - - from azure.keyvault.keys.models import KeyVaultKey - - key_id = 'https://myvault.vault.azure.net/keys/my-key/my-key-version' - key_bytes = os.urandom(32) - key = KeyVaultKey(key_id, k=key_bytes, kty='oct', key_ops=['unwrapKey', 'wrapKey']) - - Providing cryptographic material as a dictionary: - - .. code-block:: python - - from azure.keyvault.keys.models import KeyVaultKey - - key_id = 'https://myvault.vault.azure.net/keys/my-key/my-key-version' - key_bytes = os.urandom(32) - jwk = {'k': key_bytes, 'kty': 'oct', 'key_ops': ['unwrapKey', 'wrapKey']} - key = KeyVaultKey(key_id, jwk=jwk) - - """ - - def __init__(self, key_id: str, jwk: Optional[Dict[str, Any]] = None, **kwargs) -> None: - self._properties: KeyProperties = kwargs.pop("properties", None) or KeyProperties(key_id, **kwargs) - if isinstance(jwk, dict): - if any(field in kwargs for field in JsonWebKey._FIELDS): - raise ValueError( - "Individual keyword arguments for key material and the 'jwk' argument are mutually exclusive." - ) - self._key_material = JsonWebKey(**jwk) - else: - self._key_material = JsonWebKey(**kwargs) - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_key_bundle(cls, key_bundle: "_models.KeyBundle") -> "KeyVaultKey": - # pylint:disable=protected-access - return cls( - key_id=key_bundle.key.kid, # type: ignore - jwk={field: getattr(key_bundle.key, field, None) for field in JsonWebKey._FIELDS}, - properties=KeyProperties._from_key_bundle(key_bundle), - ) - - @property - def id(self) -> str: - """The key ID. - - :returns: The key ID. - :rtype: str - """ - return self._properties.id - - @property - def name(self) -> str: - """The key name. - - :returns: The key name. - :rtype: str - """ - return self._properties.name - - @property - def properties(self) -> KeyProperties: - """The key properties. - - :returns: The key properties. - :rtype: ~azure.keyvault.keys.KeyProperties - """ - return self._properties - - @property - def key(self) -> JsonWebKey: - """The JSON Web Key (JWK) for the key. - - :returns: The JSON Web Key (JWK) for the key. - :rtype: ~azure.keyvault.keys.JsonWebKey - """ - return self._key_material - - @property - def key_type(self) -> Union[str, KeyType]: - """The key's type. See :class:`~azure.keyvault.keys.KeyType` for possible values. - - :returns: The key's type. See :class:`~azure.keyvault.keys.KeyType` for possible values. - :rtype: ~azure.keyvault.keys.KeyType or str - """ - # pylint:disable=no-member - return self._key_material.kty # type: ignore[attr-defined] - - @property - def key_operations(self) -> List[Union[str, KeyOperation]]: - """Permitted operations. See :class:`~azure.keyvault.keys.KeyOperation` for possible values. - - :returns: Permitted operations. See :class:`~azure.keyvault.keys.KeyOperation` for possible values. - :rtype: List[~azure.keyvault.keys.KeyOperation or str] - """ - # pylint:disable=no-member - return self._key_material.key_ops # type: ignore[attr-defined] - - -class KeyVaultKeyIdentifier(object): - """Information about a KeyVaultKey parsed from a key ID. - - :param str source_id: The full original identifier of a key - - :raises ValueError: if the key ID is improperly formatted - - Example: - .. literalinclude:: ../tests/test_parse_id.py - :start-after: [START parse_key_vault_key_id] - :end-before: [END parse_key_vault_key_id] - :language: python - :caption: Parse a key's ID - :dedent: 8 - """ - - def __init__(self, source_id: str) -> None: - self._resource_id = parse_key_vault_id(source_id) - - @property - def source_id(self) -> str: - return self._resource_id.source_id - - @property - def vault_url(self) -> str: - return self._resource_id.vault_url - - @property - def name(self) -> str: - return self._resource_id.name - - @property - def version(self) -> Optional[str]: - return self._resource_id.version - - -class DeletedKey(KeyVaultKey): - """A deleted key's properties, cryptographic material and its deletion information. - - If soft-delete is enabled, returns information about its recovery as well. - - :param properties: Properties of the deleted key. - :type properties: ~azure.keyvault.keys.KeyProperties - :param deleted_date: When the key was deleted, in UTC. - :type deleted_date: ~datetime.datetime or None - :param recovery_id: An identifier used to recover the deleted key. Returns ``None`` if soft-delete is disabled. - :type recovery_id: str or None - :param scheduled_purge_date: When the key is scheduled to be purged, in UTC. Returns ``None`` if soft-delete is - disabled. - :type scheduled_purge_date: ~datetime.datetime or None - """ - - def __init__( - self, - properties: KeyProperties, - deleted_date: Optional[datetime] = None, - recovery_id: Optional[str] = None, - scheduled_purge_date: Optional[datetime] = None, - **kwargs: Any, - ) -> None: - super(DeletedKey, self).__init__(properties=properties, **kwargs) - self._deleted_date = deleted_date - self._recovery_id = recovery_id - self._scheduled_purge_date = scheduled_purge_date - - def __repr__(self) -> str: - return f""[:1024] - - @classmethod - def _from_deleted_key_bundle(cls, deleted_key_bundle: "_models.DeletedKeyBundle") -> "DeletedKey": - # pylint:disable=protected-access - return cls( - properties=KeyProperties._from_key_bundle(deleted_key_bundle), - key_id=deleted_key_bundle.key.kid, # type: ignore - jwk={field: getattr(deleted_key_bundle.key, field, None) for field in JsonWebKey._FIELDS}, - deleted_date=deleted_key_bundle.deleted_date, - recovery_id=deleted_key_bundle.recovery_id, - scheduled_purge_date=deleted_key_bundle.scheduled_purge_date, - ) - - @classmethod - def _from_deleted_key_item(cls, deleted_key_item: "_models.DeletedKeyItem") -> "DeletedKey": - return cls( - properties=KeyProperties._from_key_item(deleted_key_item), # pylint: disable=protected-access - key_id=deleted_key_item.kid, - deleted_date=deleted_key_item.deleted_date, - recovery_id=deleted_key_item.recovery_id, - scheduled_purge_date=deleted_key_item.scheduled_purge_date, - ) - - @property - def deleted_date(self) -> Optional[datetime]: - """When the key was deleted, in UTC. - - :returns: When the key was deleted, in UTC. - :rtype: ~datetime.datetime or None - """ - return self._deleted_date - - @property - def recovery_id(self) -> Optional[str]: - """An identifier used to recover the deleted key. Returns ``None`` if soft-delete is disabled. - - :returns: An identifier used to recover the deleted key. Returns ``None`` if soft-delete is disabled. - :rtype: str or None - """ - return self._recovery_id - - @property - def scheduled_purge_date(self) -> Optional[datetime]: - """When the key is scheduled to be purged, in UTC. Returns ``None`` if soft-delete is disabled. - - :returns: When the key is scheduled to be purged, in UTC. Returns ``None`` if soft-delete is disabled. - :rtype: ~datetime.datetime or None - """ - return self._scheduled_purge_date diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py deleted file mode 100644 index 6677724a8b67..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from ._version import VERSION - -SDK_MONIKER = f"keyvault-keys/{VERSION}" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py deleted file mode 100644 index cb088e31ad23..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Optional -from urllib import parse - -from .challenge_auth_policy import ChallengeAuthPolicy -from .client_base import KeyVaultClientBase -from .http_challenge import HttpChallenge -from . import http_challenge_cache - -HttpChallengeCache = http_challenge_cache # to avoid aliasing pylint error (C4745) - - -__all__ = [ - "ChallengeAuthPolicy", - "HttpChallenge", - "HttpChallengeCache", - "KeyVaultClientBase", -] - - -class KeyVaultResourceId: - """Represents a Key Vault identifier and its parsed contents. - - :param str source_id: The complete identifier received from Key Vault - :param str vault_url: The vault URL - :param str name: The name extracted from the ID - :param str version: The version extracted from the ID - """ - - def __init__( - self, - source_id: str, - vault_url: str, - name: str, - version: "Optional[str]" = None, - ) -> None: - self.source_id = source_id - self.vault_url = vault_url - self.name = name - self.version = version - - -def parse_key_vault_id(source_id: str) -> KeyVaultResourceId: - try: - parsed_uri = parse.urlparse(source_id) - except Exception as exc: - raise ValueError(f"'{source_id}' is not a valid ID") from exc - if not (parsed_uri.scheme and parsed_uri.hostname): - raise ValueError(f"'{source_id}' is not a valid ID") - - path = list(filter(None, parsed_uri.path.split("/"))) - - if len(path) < 2 or len(path) > 3: - raise ValueError(f"'{source_id}' is not a valid ID") - - vault_url = f"{parsed_uri.scheme}://{parsed_uri.hostname}" - if parsed_uri.port: - vault_url += f":{parsed_uri.port}" - - return KeyVaultResourceId( - source_id=source_id, - vault_url=vault_url, - name=path[1], - version=path[2] if len(path) == 3 else None, - ) - - -try: - # pylint:disable=unused-import - from .async_challenge_auth_policy import AsyncChallengeAuthPolicy - from .async_client_base import AsyncKeyVaultClientBase - - __all__.extend(["AsyncChallengeAuthPolicy", "AsyncKeyVaultClientBase"]) -except (SyntaxError, ImportError): - pass diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py deleted file mode 100644 index ff0c398bba6d..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py +++ /dev/null @@ -1,136 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import threading -import uuid -from typing import Any, Callable, cast, Optional - -from azure.core.exceptions import ResourceNotFoundError, HttpResponseError -from azure.core.pipeline import PipelineResponse -from azure.core.pipeline.transport import HttpTransport -from azure.core.polling import PollingMethod, LROPoller, NoPolling - -from azure.core.tracing.decorator import distributed_trace -from azure.core.tracing.common import with_current_context - - -class KeyVaultOperationPoller(LROPoller): - """Poller for long running operations where calling result() doesn't wait for operation to complete. - - :param polling_method: The poller's polling method. - :type polling_method: ~azure.core.polling.PollingMethod - """ - - def __init__(self, polling_method: PollingMethod) -> None: - super(KeyVaultOperationPoller, self).__init__(None, None, lambda *_: None, NoPolling()) - self._polling_method = polling_method - - # pylint: disable=arguments-differ - def result(self) -> "Any": # type: ignore - """Returns a representation of the final resource without waiting for the operation to complete. - - :returns: The deserialized resource of the long running operation - :rtype: Any - - :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. - """ - return self._polling_method.resource() - - @distributed_trace - def wait(self, timeout: Optional[float] = None) -> None: - """Wait on the long running operation for a number of seconds. - - You can check if this call has ended with timeout with the "done()" method. - - :param float timeout: Period of time to wait for the long running operation to complete (in seconds). - - :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. - """ - - if not self._polling_method.finished(): - self._done = threading.Event() - self._thread = threading.Thread( - target=with_current_context(self._start), name=f"KeyVaultOperationPoller({uuid.uuid4()})" - ) - self._thread.daemon = True - self._thread.start() - - if self._thread is None: - return - self._thread.join(timeout=timeout) - try: - # Let's handle possible None in forgiveness here - raise self._exception # type: ignore - except TypeError: # Was None - pass - - -class DeleteRecoverPollingMethod(PollingMethod): - """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. - - This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault - immediately removes it from its collection. However, the resource will not immediately appear in the deleted - collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, - the resource exists in the deleted collection i.e. its deletion is complete. - - Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted - resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. - - :param pipeline_response: The operation's original pipeline response. - :type pipeline_response: PipelineResponse - :param command: A callable to invoke when polling. - :type command: Callable - :param final_resource: The final resource returned by the polling operation. - :type final_resource: Any - :param bool finished: Whether or not the polling operation is completed. - :param int interval: The polling interval, in seconds. - """ - - def __init__( - self, - pipeline_response: PipelineResponse, - command: Callable, - final_resource: Any, - finished: bool, - interval: int = 2, - ) -> None: - self._pipeline_response = pipeline_response - self._command = command - self._resource = final_resource - self._polling_interval = interval - self._finished = finished - - def _update_status(self) -> None: - try: - self._command() - self._finished = True - except ResourceNotFoundError: - pass - except HttpResponseError as e: - # If we are polling on get_deleted_* and we don't have get permissions, we will get - # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. - if e.status_code == 403: - self._finished = True - else: - raise - - def initialize(self, client: Any, initial_response: Any, deserialization_callback: Callable) -> None: - pass - - def run(self) -> None: - while not self.finished(): - self._update_status() - if not self.finished(): - # We should always ask the client's transport to sleep, instead of sleeping directly - transport: HttpTransport = cast(HttpTransport, self._pipeline_response.context.transport) - transport.sleep(self._polling_interval) - - def finished(self) -> bool: - return self._finished - - def resource(self) -> Any: - return self._resource - - def status(self) -> str: - return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py deleted file mode 100644 index 16168229af08..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py +++ /dev/null @@ -1,80 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Any, Callable, cast - -from azure.core.exceptions import ResourceNotFoundError, HttpResponseError -from azure.core.pipeline import PipelineResponse -from azure.core.pipeline.transport import AsyncHttpTransport -from azure.core.polling import AsyncPollingMethod - - -class AsyncDeleteRecoverPollingMethod(AsyncPollingMethod): - """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. - - This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault - immediately removes it from its collection. However, the resource will not immediately appear in the deleted - collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, - the resource exists in the deleted collection i.e. its deletion is complete. - - Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted - resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. - - :param pipeline_response: The operation's original pipeline response. - :type pipeline_response: PipelineResponse - :param command: An awaitable to invoke when polling. - :type command: Callable - :param final_resource: The final resource returned by the polling operation. - :type final_resource: Any - :param bool finished: Whether or not the polling operation is completed. - :param int interval: The polling interval, in seconds. - """ - - def __init__( - self, - pipeline_response: PipelineResponse, - command: Callable, - final_resource: Any, - finished: bool, - interval: int = 2, - ) -> None: - self._pipeline_response = pipeline_response - self._command = command - self._resource = final_resource - self._polling_interval = interval - self._finished = finished - - def initialize(self, client, initial_response, deserialization_callback): - pass - - async def _update_status(self) -> None: - try: - await self._command() - self._finished = True - except ResourceNotFoundError: - pass - except HttpResponseError as e: - # If we are polling on get_deleted_* and we don't have get permissions, we will get - # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. - if e.status_code == 403: - self._finished = True - else: - raise - - async def run(self) -> None: - while not self.finished(): - await self._update_status() - if not self.finished(): - # We should always ask the client's transport to sleep, instead of sleeping directly - transport: AsyncHttpTransport = cast(AsyncHttpTransport, self._pipeline_response.context.transport) - await transport.sleep(self._polling_interval) - - def finished(self) -> bool: - return self._finished - - def resource(self) -> Any: - return self._resource - - def status(self) -> str: - return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py deleted file mode 100644 index 3e3ac1855178..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py +++ /dev/null @@ -1,256 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""Policy implementing Key Vault's challenge authentication protocol. - -Normally the protocol is only used for the client's first service request, upon which: -1. The challenge authentication policy sends a copy of the request, without authorization or content. -2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. -3. The policy authenticates according to the challenge and sends the original request with authorization. - -The policy caches the challenge and thus knows how to authenticate future requests. However, authentication -requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the -protocol again. -""" - -from copy import deepcopy -import sys -import time -from typing import Any, Callable, cast, Optional, overload, TypeVar, Union -from urllib.parse import urlparse - -from typing_extensions import ParamSpec - -from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions -from azure.core.credentials_async import AsyncSupportsTokenInfo, AsyncTokenCredential, AsyncTokenProvider -from azure.core.pipeline import PipelineRequest, PipelineResponse -from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from azure.core.rest import AsyncHttpResponse, HttpRequest - -from .http_challenge import HttpChallenge -from . import http_challenge_cache as ChallengeCache -from .challenge_auth_policy import _enforce_tls, _has_claims, _update_challenge - -if sys.version_info < (3, 9): - from typing import Awaitable -else: - from collections.abc import Awaitable - - -P = ParamSpec("P") -T = TypeVar("T") - - -@overload -async def await_result(func: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T: ... - - -@overload -async def await_result(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ... - - -async def await_result(func: Callable[P, Union[T, Awaitable[T]]], *args: P.args, **kwargs: P.kwargs) -> T: - """If func returns an awaitable, await it. - - :param func: The function to run. - :type func: callable - :param args: The positional arguments to pass to the function. - :type args: list - :rtype: any - :return: The result of the function - """ - result = func(*args, **kwargs) - if isinstance(result, Awaitable): - return await result - return result - - -class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): - """Policy for handling HTTP authentication challenges. - - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity.aio` - :type credential: ~azure.core.credentials_async.AsyncTokenProvider - """ - - def __init__(self, credential: AsyncTokenProvider, *scopes: str, **kwargs: Any) -> None: - # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request - super().__init__(credential, *scopes, enable_cae=True, **kwargs) - self._credential: AsyncTokenProvider = credential - self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None - self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) - self._request_copy: Optional[HttpRequest] = None - - async def send(self, request: PipelineRequest[HttpRequest]) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: - """Authorize request with a bearer token and send it to the next policy. - - We implement this method to account for the valid scenario where a Key Vault authentication challenge is - immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to - the caller, but we should handle that second challenge as well (and only return any third 401 response). - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - await await_result(self.on_request, request) - response: PipelineResponse[HttpRequest, AsyncHttpResponse] - try: - response = await self.next.send(request) - except Exception: # pylint:disable=broad-except - await await_result(self.on_exception, request) - raise - await await_result(self.on_response, request, response) - - if response.http_response.status_code == 401: - return await self.handle_challenge_flow(request, response) - return response - - async def handle_challenge_flow( - self, - request: PipelineRequest[HttpRequest], - response: PipelineResponse[HttpRequest, AsyncHttpResponse], - consecutive_challenge: bool = False, - ) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: - """Handle the challenge flow of Key Vault and CAE authentication. - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - :param response: The pipeline response object - :type response: ~azure.core.pipeline.PipelineResponse - :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. - Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. - True if the preceding challenge was a Key Vault challenge; False otherwise. - - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - self._token = None # any cached token is invalid - if "WWW-Authenticate" in response.http_response.headers: - # If the previous challenge was a KV challenge and this one is too, return the 401 - claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) - if consecutive_challenge and not claims_challenge: - return response - - request_authorized = await self.on_challenge(request, response) - if request_authorized: - # if we receive a challenge response, we retrieve a new token - # which matches the new target. In this case, we don't want to remove - # token from the request so clear the 'insecure_domain_change' tag - request.context.options.pop("insecure_domain_change", False) - try: - response = await self.next.send(request) - except Exception: # pylint:disable=broad-except - await await_result(self.on_exception, request) - raise - - # If consecutive_challenge == True, this could be a third consecutive 401 - if response.http_response.status_code == 401 and not consecutive_challenge: - # If the previous challenge wasn't from CAE, we can try this function one more time - if not claims_challenge: - return await self.handle_challenge_flow(request, response, consecutive_challenge=True) - await await_result(self.on_response, request, response) - return response - - async def on_request(self, request: PipelineRequest) -> None: - _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if challenge: - # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. - if self._need_new_token(): - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - await self._request_kv_token(scope, challenge) - - bearer_token = cast(Union[AccessToken, AccessTokenInfo], self._token).token - request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" - return - - # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, - # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. - # on_challenge will parse that challenge, use the original request including the body, authorize the - # request, and tell super to send it again. - if request.http_request.content: - self._request_copy = request.http_request - bodiless_request = HttpRequest( - method=request.http_request.method, - url=request.http_request.url, - headers=deepcopy(request.http_request.headers), - ) - bodiless_request.headers["Content-Length"] = "0" - request.http_request = bodiless_request - - async def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: - try: - # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary - old_scope: Optional[str] = None - old_tenant: Optional[str] = None - cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if cached_challenge: - old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" - old_tenant = cached_challenge.tenant_id - - challenge = _update_challenge(request, response) - # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary - if challenge.claims and old_scope: - challenge._parameters["scope"] = old_scope # pylint:disable=protected-access - challenge.tenant_id = old_tenant - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - except ValueError: - return False - - if self._verify_challenge_resource: - resource_domain = urlparse(scope).netloc - if not resource_domain: - raise ValueError(f"The challenge contains invalid scope '{scope}'.") - - request_domain = urlparse(request.http_request.url).netloc - if not request_domain.lower().endswith(f".{resource_domain.lower()}"): - raise ValueError( - f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " - "`verify_challenge_resource=False` to your client's constructor to disable this verification. " - "See https://aka.ms/azsdk/blog/vault-uri for more information." - ) - - # If we had created a request copy in on_request, use it now to send along the original body content - if self._request_copy: - request.http_request = self._request_copy - - # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication - # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 - if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): - await self.authorize_request(request, scope, claims=challenge.claims) - else: - await self.authorize_request(request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id) - - return True - - def _need_new_token(self) -> bool: - now = time.time() - refresh_on = getattr(self._token, "refresh_on", None) - return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 - - async def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: - """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. - - :param str scope: The scope for which to request a token. - :param challenge: The challenge for the request being made. - :type challenge: HttpChallenge - """ - # Exclude tenant for AD FS authentication - exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") - # The AsyncSupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs - if hasattr(self._credential, "get_token_info"): - options: TokenRequestOptions = {"enable_cae": True} - if challenge.tenant_id and not exclude_tenant: - options["tenant_id"] = challenge.tenant_id - self._token = await cast(AsyncSupportsTokenInfo, self._credential).get_token_info(scope, options=options) - else: - if exclude_tenant: - self._token = await self._credential.get_token(scope, enable_cae=True) - else: - self._token = await cast(AsyncTokenCredential, self._credential).get_token( - scope, tenant_id=challenge.tenant_id, enable_cae=True - ) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py deleted file mode 100644 index 3e1a2bec8fc4..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py +++ /dev/null @@ -1,117 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import sys -from typing import Any - -from azure.core.credentials_async import AsyncTokenCredential -from azure.core.pipeline.policies import HttpLoggingPolicy -from azure.core.rest import AsyncHttpResponse, HttpRequest -from azure.core.tracing.decorator_async import distributed_trace_async - -from . import AsyncChallengeAuthPolicy -from .client_base import ApiVersion, DEFAULT_VERSION, _format_api_version, _SERIALIZER -from .._sdk_moniker import SDK_MONIKER -from .._generated.aio import KeyVaultClient as _KeyVaultClient -from .._generated import models as _models - -if sys.version_info < (3, 9): - from typing import Awaitable -else: - from collections.abc import Awaitable - - -class AsyncKeyVaultClientBase(object): - # pylint:disable=protected-access - def __init__(self, vault_url: str, credential: AsyncTokenCredential, **kwargs: Any) -> None: - if not credential: - raise ValueError( - "credential should be an object supporting the AsyncTokenCredential protocol, " - "such as a credential from azure-identity" - ) - if not vault_url: - raise ValueError("vault_url must be the URL of an Azure Key Vault") - - try: - self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) - # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility - if hasattr(self.api_version, "value"): - self.api_version = self.api_version.value - self._vault_url = vault_url.strip(" /") - - client = kwargs.get("generated_client") - if client: - # caller provided a configured client -> only models left to initialize - self._client = client - models = kwargs.get("generated_models") - self._models = models or _models - return - - http_logging_policy = HttpLoggingPolicy(**kwargs) - http_logging_policy.allowed_header_names.update( - {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} - ) - - verify_challenge = kwargs.pop("verify_challenge_resource", True) - self._client = _KeyVaultClient( - credential=credential, - vault_base_url=self._vault_url, - api_version=self.api_version, - authentication_policy=AsyncChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), - sdk_moniker=SDK_MONIKER, - http_logging_policy=http_logging_policy, - **kwargs, - ) - self._models = _models - except ValueError as exc: - # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum - raise NotImplementedError( - f"This package doesn't support API version '{self.api_version}'. " - + "Supported versions: " - + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] - ) from exc - - @property - def vault_url(self) -> str: - return self._vault_url - - async def __aenter__(self) -> "AsyncKeyVaultClientBase": - await self._client.__aenter__() - return self - - async def __aexit__(self, *args: Any) -> None: - await self._client.__aexit__(*args) - - async def close(self) -> None: - """Close sockets opened by the client. - - Calling this method is unnecessary when using the client as a context manager. - """ - await self._client.close() - - @distributed_trace_async - def send_request( - self, request: HttpRequest, *, stream: bool = False, **kwargs: Any - ) -> Awaitable[AsyncHttpResponse]: - """Runs a network request using the client's existing pipeline. - - The request URL can be relative to the vault URL. The service API version used for the request is the same as - the client's unless otherwise specified. This method does not raise if the response is an error; to raise an - exception, call `raise_for_status()` on the returned response object. For more information about how to send - custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. - - :param request: The network request you want to make. - :type request: ~azure.core.rest.HttpRequest - - :keyword bool stream: Whether the response payload will be streamed. Defaults to False. - - :return: The response of your network call. Does not do error handling on your response. - :rtype: ~azure.core.rest.AsyncHttpResponse - """ - request_copy = _format_api_version(request, self.api_version) - path_format_arguments = { - "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), - } - request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) - return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py deleted file mode 100644 index eb4073d0e699..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py +++ /dev/null @@ -1,270 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""Policy implementing Key Vault's challenge authentication protocol. - -Normally the protocol is only used for the client's first service request, upon which: -1. The challenge authentication policy sends a copy of the request, without authorization or content. -2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. -3. The policy authenticates according to the challenge and sends the original request with authorization. - -The policy caches the challenge and thus knows how to authenticate future requests. However, authentication -requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the -protocol again. -""" - -from copy import deepcopy -import time -from typing import Any, cast, Optional, Union -from urllib.parse import urlparse - -from azure.core.credentials import ( - AccessToken, - AccessTokenInfo, - TokenCredential, - TokenProvider, - TokenRequestOptions, - SupportsTokenInfo, -) -from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineRequest, PipelineResponse -from azure.core.pipeline.policies import BearerTokenCredentialPolicy -from azure.core.rest import HttpRequest, HttpResponse - -from .http_challenge import HttpChallenge -from . import http_challenge_cache as ChallengeCache - - -def _enforce_tls(request: PipelineRequest) -> None: - if not request.http_request.url.lower().startswith("https"): - raise ServiceRequestError( - "Bearer token authentication is not permitted for non-TLS protected (non-https) URLs." - ) - - -def _has_claims(challenge: str) -> bool: - """Check if a challenge header contains claims. - - :param challenge: The challenge header to check. - :type challenge: str - - :returns: True if the challenge contains claims; False otherwise. - :rtype: bool - """ - # Split the challenge into its scheme and parameters, then check if any parameter contains claims - split_challenge = challenge.strip().split(" ", 1) - return any("claims=" in item for item in split_challenge[1].split(",")) - - -def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) -> HttpChallenge: - """Parse challenge from a challenge response, cache it, and return it. - - :param request: The pipeline request that prompted the challenge response. - :type request: ~azure.core.pipeline.PipelineRequest - :param challenger: The pipeline response containing the authentication challenge. - :type challenger: ~azure.core.pipeline.PipelineResponse - - :returns: An HttpChallenge object representing the authentication challenge. - :rtype: HttpChallenge - """ - - challenge = HttpChallenge( - request.http_request.url, - challenger.http_response.headers.get("WWW-Authenticate"), - response_headers=challenger.http_response.headers, - ) - ChallengeCache.set_challenge_for_url(request.http_request.url, challenge) - return challenge - - -class ChallengeAuthPolicy(BearerTokenCredentialPolicy): - """Policy for handling HTTP authentication challenges. - - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity` - :type credential: ~azure.core.credentials.TokenProvider - :param str scopes: Lets you specify the type of access needed. - """ - - def __init__(self, credential: TokenProvider, *scopes: str, **kwargs: Any) -> None: - # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request - super(ChallengeAuthPolicy, self).__init__(credential, *scopes, enable_cae=True, **kwargs) - self._credential: TokenProvider = credential - self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None - self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) - self._request_copy: Optional[HttpRequest] = None - - def send(self, request: PipelineRequest[HttpRequest]) -> PipelineResponse[HttpRequest, HttpResponse]: - """Authorize request with a bearer token and send it to the next policy. - - We implement this method to account for the valid scenario where a Key Vault authentication challenge is - immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to - the caller, but we should handle that second challenge as well (and only return any third 401 response). - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - self.on_request(request) - try: - response = self.next.send(request) - except Exception: # pylint:disable=broad-except - self.on_exception(request) - raise - - self.on_response(request, response) - if response.http_response.status_code == 401: - return self.handle_challenge_flow(request, response) - return response - - def handle_challenge_flow( - self, - request: PipelineRequest[HttpRequest], - response: PipelineResponse[HttpRequest, HttpResponse], - consecutive_challenge: bool = False, - ) -> PipelineResponse[HttpRequest, HttpResponse]: - """Handle the challenge flow of Key Vault and CAE authentication. - - :param request: The pipeline request object - :type request: ~azure.core.pipeline.PipelineRequest - :param response: The pipeline response object - :type response: ~azure.core.pipeline.PipelineResponse - :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. - Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. - True if the preceding challenge was a Key Vault challenge; False otherwise. - - :return: The pipeline response object - :rtype: ~azure.core.pipeline.PipelineResponse - """ - self._token = None # any cached token is invalid - if "WWW-Authenticate" in response.http_response.headers: - # If the previous challenge was a KV challenge and this one is too, return the 401 - claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) - if consecutive_challenge and not claims_challenge: - return response - - request_authorized = self.on_challenge(request, response) - if request_authorized: - # if we receive a challenge response, we retrieve a new token - # which matches the new target. In this case, we don't want to remove - # token from the request so clear the 'insecure_domain_change' tag - request.context.options.pop("insecure_domain_change", False) - try: - response = self.next.send(request) - except Exception: # pylint:disable=broad-except - self.on_exception(request) - raise - - # If consecutive_challenge == True, this could be a third consecutive 401 - if response.http_response.status_code == 401 and not consecutive_challenge: - # If the previous challenge wasn't from CAE, we can try this function one more time - if not claims_challenge: - return self.handle_challenge_flow(request, response, consecutive_challenge=True) - self.on_response(request, response) - return response - - def on_request(self, request: PipelineRequest) -> None: - _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if challenge: - # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. - if self._need_new_token: - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._request_kv_token(scope, challenge) - - bearer_token = cast(Union["AccessToken", "AccessTokenInfo"], self._token).token - request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" - return - - # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, - # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. - # on_challenge will parse that challenge, use the original request including the body, authorize the - # request, and tell super to send it again. - if request.http_request.content: - self._request_copy = request.http_request - bodiless_request = HttpRequest( - method=request.http_request.method, - url=request.http_request.url, - headers=deepcopy(request.http_request.headers), - ) - bodiless_request.headers["Content-Length"] = "0" - request.http_request = bodiless_request - - def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: - try: - # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary - old_scope: Optional[str] = None - old_tenant: Optional[str] = None - cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if cached_challenge: - old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" - old_tenant = cached_challenge.tenant_id - - challenge = _update_challenge(request, response) - # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary - if challenge.claims and old_scope: - challenge._parameters["scope"] = old_scope # pylint:disable=protected-access - challenge.tenant_id = old_tenant - # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource - scope = challenge.get_scope() or challenge.get_resource() + "/.default" - except ValueError: - return False - - if self._verify_challenge_resource: - resource_domain = urlparse(scope).netloc - if not resource_domain: - raise ValueError(f"The challenge contains invalid scope '{scope}'.") - - request_domain = urlparse(request.http_request.url).netloc - if not request_domain.lower().endswith(f".{resource_domain.lower()}"): - raise ValueError( - f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " - "`verify_challenge_resource=False` to your client's constructor to disable this verification. " - "See https://aka.ms/azsdk/blog/vault-uri for more information." - ) - - # If we had created a request copy in on_request, use it now to send along the original body content - if self._request_copy: - request.http_request = self._request_copy - - # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication - # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 - if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): - self.authorize_request(request, scope, claims=challenge.claims) - else: - self.authorize_request(request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id) - - return True - - @property - def _need_new_token(self) -> bool: - now = time.time() - refresh_on = getattr(self._token, "refresh_on", None) - return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 - - def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: - """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. - - :param str scope: The scope for which to request a token. - :param challenge: The challenge for the request being made. - :type challenge: HttpChallenge - """ - # Exclude tenant for AD FS authentication - exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") - # The SupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs - if hasattr(self._credential, "get_token_info"): - options: TokenRequestOptions = {"enable_cae": True} - if challenge.tenant_id and not exclude_tenant: - options["tenant_id"] = challenge.tenant_id - self._token = cast(SupportsTokenInfo, self._credential).get_token_info(scope, options=options) - else: - if exclude_tenant: - self._token = self._credential.get_token(scope, enable_cae=True) - else: - self._token = cast(TokenCredential, self._credential).get_token( - scope, tenant_id=challenge.tenant_id, enable_cae=True - ) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py deleted file mode 100644 index 9e721ef6cfe2..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py +++ /dev/null @@ -1,162 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from copy import deepcopy -from enum import Enum -from typing import Any -from urllib.parse import urlparse - -from azure.core import CaseInsensitiveEnumMeta -from azure.core.credentials import TokenCredential -from azure.core.pipeline.policies import HttpLoggingPolicy -from azure.core.rest import HttpRequest, HttpResponse -from azure.core.tracing.decorator import distributed_trace - -from . import ChallengeAuthPolicy -from .._generated import KeyVaultClient as _KeyVaultClient -from .._generated import models as _models -from .._generated._utils.serialization import Serializer -from .._sdk_moniker import SDK_MONIKER - - -class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Key Vault API versions supported by this package""" - - #: this is the default version - V2025_07_01 = "2025-07-01" - V7_6 = "7.6" - V7_5 = "7.5" - V7_4 = "7.4" - V7_3 = "7.3" - V7_2 = "7.2" - V7_1 = "7.1" - V7_0 = "7.0" - V2016_10_01 = "2016-10-01" - - -DEFAULT_VERSION = ApiVersion.V2025_07_01 - -_SERIALIZER = Serializer() -_SERIALIZER.client_side_validation = False - - -def _format_api_version(request: HttpRequest, api_version: str) -> HttpRequest: - """Returns a request copy that includes an api-version query parameter if one wasn't originally present. - - :param request: The HTTP request being sent. - :type request: ~azure.core.rest.HttpRequest - :param str api_version: The service API version that the request should include. - - :returns: A copy of the request that includes an api-version query parameter. - :rtype: azure.core.rest.HttpRequest - """ - request_copy = deepcopy(request) - params = {"api-version": api_version} # By default, we want to use the client's API version - query = urlparse(request_copy.url).query - - if query: - request_copy.url = request_copy.url.partition("?")[0] - existing_params = {p[0]: p[-1] for p in [p.partition("=") for p in query.split("&")]} - params.update(existing_params) # If an api-version was provided, this will overwrite our default - - # Reconstruct the query parameters onto the URL - query_params = [] - for k, v in params.items(): - query_params.append("{}={}".format(k, v)) - query = "?" + "&".join(query_params) - request_copy.url = request_copy.url + query - return request_copy - - -class KeyVaultClientBase(object): - # pylint:disable=protected-access - def __init__(self, vault_url: str, credential: TokenCredential, **kwargs: Any) -> None: - if not credential: - raise ValueError( - "credential should be an object supporting the TokenCredential protocol, " - "such as a credential from azure-identity" - ) - if not vault_url: - raise ValueError("vault_url must be the URL of an Azure Key Vault") - - try: - self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) - # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility - if hasattr(self.api_version, "value"): - self.api_version = self.api_version.value - self._vault_url = vault_url.strip(" /") - - client = kwargs.get("generated_client") - if client: - # caller provided a configured client -> only models left to initialize - self._client = client - models = kwargs.get("generated_models") - self._models = models or _models - return - - http_logging_policy = HttpLoggingPolicy(**kwargs) - http_logging_policy.allowed_header_names.update( - {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} - ) - - verify_challenge = kwargs.pop("verify_challenge_resource", True) - self._client = _KeyVaultClient( - credential=credential, - vault_base_url=self._vault_url, - api_version=self.api_version, - authentication_policy=ChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), - sdk_moniker=SDK_MONIKER, - http_logging_policy=http_logging_policy, - **kwargs, - ) - self._models = _models - except ValueError as exc: - # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum - raise NotImplementedError( - f"This package doesn't support API version '{self.api_version}'. " - + "Supported versions: " - + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] - ) from exc - - @property - def vault_url(self) -> str: - return self._vault_url - - def __enter__(self) -> "KeyVaultClientBase": - self._client.__enter__() - return self - - def __exit__(self, *args: Any) -> None: - self._client.__exit__(*args) - - def close(self) -> None: - """Close sockets opened by the client. - - Calling this method is unnecessary when using the client as a context manager. - """ - self._client.close() - - @distributed_trace - def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: - """Runs a network request using the client's existing pipeline. - - The request URL can be relative to the vault URL. The service API version used for the request is the same as - the client's unless otherwise specified. This method does not raise if the response is an error; to raise an - exception, call `raise_for_status()` on the returned response object. For more information about how to send - custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. - - :param request: The network request you want to make. - :type request: ~azure.core.rest.HttpRequest - - :keyword bool stream: Whether the response payload will be streamed. Defaults to False. - - :return: The response of your network call. Does not do error handling on your response. - :rtype: ~azure.core.rest.HttpResponse - """ - request_copy = _format_api_version(request, self.api_version) - path_format_arguments = { - "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), - } - request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) - return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py deleted file mode 100644 index 8b14b999de78..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py +++ /dev/null @@ -1,186 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import base64 -from typing import Dict, MutableMapping, Optional -from urllib import parse - - -class HttpChallenge(object): - """An object representing the content of a Key Vault authentication challenge. - - :param str request_uri: The URI of the HTTP request that prompted this challenge. - :param str challenge: The WWW-Authenticate header of the challenge response. - :param response_headers: Optional. The headers attached to the challenge response. - :type response_headers: MutableMapping[str, str] or None - """ - - def __init__( - self, request_uri: str, challenge: str, response_headers: "Optional[MutableMapping[str, str]]" = None - ) -> None: - """Parses an HTTP WWW-Authentication Bearer challenge from a server. - - Example challenge with claims: - Bearer authorization="https://login.windows-ppe.net/", error="invalid_token", - error_description="User session has been revoked", - claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0=" - """ - self.source_authority = self._validate_request_uri(request_uri) - self.source_uri = request_uri - self._parameters: "Dict[str, str]" = {} - - # get the scheme of the challenge and remove from the challenge string - trimmed_challenge = self._validate_challenge(challenge) - split_challenge = trimmed_challenge.split(" ", 1) - self.scheme = split_challenge[0] - trimmed_challenge = split_challenge[1] - - self.claims = None - # split trimmed challenge into comma-separated name=value pairs. Values are expected - # to be surrounded by quotes which are stripped here. - for item in trimmed_challenge.split(","): - # Special case for claims, which can contain = symbols as padding. Assume at most one claim per challenge - if "claims=" in item: - encoded_claims = item[item.index("=") + 1 :].strip(" \"'") - padding_needed = -len(encoded_claims) % 4 - try: - decoded_claims = base64.urlsafe_b64decode(encoded_claims + "=" * padding_needed).decode() - self.claims = decoded_claims - except Exception: # pylint:disable=broad-except - continue - # process name=value pairs - else: - comps = item.split("=") - if len(comps) == 2: - key = comps[0].strip(' "') - value = comps[1].strip(' "') - if key: - self._parameters[key] = value - - # minimum set of parameters - if not self._parameters: - raise ValueError("Invalid challenge parameters") - - # must specify authorization or authorization_uri - if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: - raise ValueError("Invalid challenge parameters") - - authorization_uri = self.get_authorization_server() - # the authorization server URI should look something like https://login.windows.net/tenant-id - raw_uri_path = str(parse.urlparse(authorization_uri).path) - uri_path = raw_uri_path.lstrip("/") - self.tenant_id = uri_path.split("/", maxsplit=1)[0] or None - - # if the response headers were supplied - if response_headers: - # get the message signing key and message key encryption key from the headers - self.server_signature_key = response_headers.get("x-ms-message-signing-key", None) - self.server_encryption_key = response_headers.get("x-ms-message-encryption-key", None) - - def is_bearer_challenge(self) -> bool: - """Tests whether the HttpChallenge is a Bearer challenge. - - :returns: True if the challenge is a Bearer challenge; False otherwise. - :rtype: bool - """ - if not self.scheme: - return False - - return self.scheme.lower() == "bearer" - - def is_pop_challenge(self) -> bool: - """Tests whether the HttpChallenge is a proof of possession challenge. - - :returns: True if the challenge is a proof of possession challenge; False otherwise. - :rtype: bool - """ - if not self.scheme: - return False - - return self.scheme.lower() == "pop" - - def get_value(self, key: str) -> "Optional[str]": - return self._parameters.get(key) - - def get_authorization_server(self) -> str: - """Returns the URI for the authorization server if present, otherwise an empty string. - - :returns: The URI for the authorization server if present, otherwise an empty string. - :rtype: str - """ - value = "" - for key in ["authorization_uri", "authorization"]: - value = self.get_value(key) or "" - if value: - break - return value - - def get_resource(self) -> str: - """Returns the resource if present, otherwise an empty string. - - :returns: The challenge resource if present, otherwise an empty string. - :rtype: str - """ - return self.get_value("resource") or "" - - def get_scope(self) -> str: - """Returns the scope if present, otherwise an empty string. - - :returns: The challenge scope if present, otherwise an empty string. - :rtype: str - """ - return self.get_value("scope") or "" - - def supports_pop(self) -> bool: - """Returns True if the challenge supports proof of possession token auth; False otherwise. - - :returns: True if the challenge supports proof of possession token auth; False otherwise. - :rtype: bool - """ - return self._parameters.get("supportspop", "").lower() == "true" - - def supports_message_protection(self) -> bool: - """Returns True if the challenge vault supports message protection; False otherwise. - - :returns: True if the challenge vault supports message protection; False otherwise. - :rtype: bool - """ - return self.supports_pop() and self.server_encryption_key and self.server_signature_key # type: ignore - - def _validate_challenge( - self, challenge: str - ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use - """Verifies that the challenge is a valid auth challenge and returns the key=value pairs. - - :param str challenge: The WWW-Authenticate header of the challenge response. - - :returns: The challenge key/value pairs, with whitespace removed, as a string. - :rtype: str - """ - if not challenge: - raise ValueError("Challenge cannot be empty") - - return challenge.strip() - - def _validate_request_uri( - self, uri: str - ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use - """Extracts the host authority from the given URI. - - :param str uri: The URI of the HTTP request that prompted the challenge. - - :returns: The challenge host authority. - :rtype: str - """ - if not uri: - raise ValueError("request_uri cannot be empty") - - parsed = parse.urlparse(uri) - if not parsed.netloc: - raise ValueError("request_uri must be an absolute URI") - - if parsed.scheme.lower() not in ["http", "https"]: - raise ValueError("request_uri must be HTTP or HTTPS") - - return parsed.netloc diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py deleted file mode 100644 index 99f32091e24b..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py +++ /dev/null @@ -1,93 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import threading -from typing import Dict, Optional -from urllib import parse - -from .http_challenge import HttpChallenge - - -_cache: "Dict[str, HttpChallenge]" = {} -_lock = threading.Lock() - - -def get_challenge_for_url(url: str) -> "Optional[HttpChallenge]": - """Gets the challenge for the cached URL. - - :param str url: the URL the challenge is cached for. - - :returns: The challenge for the cached request URL, or None if the request URL isn't cached. - :rtype: HttpChallenge or None - """ - - if not url: - raise ValueError("URL cannot be None") - - key = _get_cache_key(url) - - with _lock: - return _cache.get(key.lower()) - - -def _get_cache_key(url: str) -> str: - """Use the URL's netloc as cache key except when the URL specifies the default port for its scheme. In that case - use the netloc without the port. That is to say, https://foo.bar and https://foo.bar:443 are considered equivalent. - - This equivalency prevents an unnecessary challenge when using Key Vault's paging API. The Key Vault client doesn't - specify ports, but Key Vault's next page links do, so a redundant challenge would otherwise be executed when the - client requests the next page. - - :param str url: The HTTP request URL. - - :returns: The URL's `netloc`, minus any port attached to the URL. - :rtype: str - """ - - parsed = parse.urlparse(url) - if parsed.scheme == "https" and parsed.port == 443: - return parsed.netloc[:-4] - return parsed.netloc - - -def remove_challenge_for_url(url: str) -> None: - """Removes the cached challenge for the specified URL. - - :param str url: the URL for which to remove the cached challenge - """ - if not url: - raise ValueError("URL cannot be empty") - - key = _get_cache_key(url) - with _lock: - del _cache[key.lower()] - - -def set_challenge_for_url(url: str, challenge: "HttpChallenge") -> None: - """Caches the challenge for the specified URL. - - :param str url: the URL for which to cache the challenge - :param challenge: the challenge to cache - :type challenge: HttpChallenge - """ - if not url: - raise ValueError("URL cannot be empty") - - if not challenge: - raise ValueError("Challenge cannot be empty") - - src_url = parse.urlparse(url) - if src_url.netloc.lower() != challenge.source_authority.lower(): - raise ValueError("Source URL and Challenge URL do not match") - - key = _get_cache_key(url) - with _lock: - _cache[key.lower()] = challenge - - -def clear() -> None: - """Clears the cache.""" - - with _lock: - _cache.clear() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py deleted file mode 100644 index de39d939367f..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py +++ /dev/null @@ -1,6 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -VERSION = "4.11.1" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py deleted file mode 100644 index 71cad7e66b18..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from ._client import KeyClient - -__all__ = ["KeyClient"] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py deleted file mode 100644 index 41084614a416..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py +++ /dev/null @@ -1,1017 +0,0 @@ -# pylint: disable=too-many-lines -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -# pylint:disable=too-many-lines -from datetime import datetime -from functools import partial -from typing import Any, Dict, List, Optional, Union - -from azure.core.async_paging import AsyncItemPaged -from azure.core.tracing.decorator import distributed_trace -from azure.core.tracing.decorator_async import distributed_trace_async - -from ..crypto.aio import CryptographyClient -from .._client import _get_key_id -from .._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation -from .._generated.models import KeyAttributes -from .._shared._polling_async import AsyncDeleteRecoverPollingMethod -from .._shared import AsyncKeyVaultClientBase -from .. import ( - DeletedKey, - JsonWebKey, - KeyProperties, - KeyReleasePolicy, - KeyRotationLifetimeAction, - KeyRotationPolicy, - KeyType, - KeyVaultKey, - ReleaseKeyResult, -) - - -class KeyClient(AsyncKeyVaultClientBase): - """A high-level asynchronous interface for managing a vault's keys. - - :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". - You should validate that this URL references a valid Key Vault or Managed HSM resource. - See https://aka.ms/azsdk/blog/vault-uri for details. - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity.aio` - :type credential: ~azure.core.credentials_async.AsyncTokenCredential - - :keyword api_version: Version of the service API to use. Defaults to the most recent. - :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str - :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key - Vault or Managed HSM domain. Defaults to True. - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START create_key_client] - :end-before: [END create_key_client] - :language: python - :caption: Create a new ``KeyClient`` - :dedent: 4 - """ - - # pylint:disable=protected-access, too-many-public-methods - - def _get_attributes( - self, - enabled: Optional[bool], - not_before: Optional[datetime], - expires_on: Optional[datetime], - exportable: Optional[bool] = None, - ) -> Optional[KeyAttributes]: - """Return a KeyAttributes object if non-None attributes are provided, or None otherwise. - - :param enabled: Whether the key is enabled. - :type enabled: bool or None - :param not_before: Not before date of the key in UTC. - :type not_before: ~datetime.datetime or None - :param expires_on: Expiry date of the key in UTC. - :type expires_on: ~datetime.datetime or None - :param exportable: Whether the private key can be exported. - :type exportable: bool or None - - :returns: An autorest-generated model of the key's attributes. - :rtype: KeyAttributes - """ - if enabled is not None or not_before is not None or expires_on is not None or exportable is not None: - return self._models.KeyAttributes( - enabled=enabled, not_before=not_before, expires=expires_on, exportable=exportable - ) - return None - - def get_cryptography_client( - self, - key_name: str, - *, - key_version: Optional[str] = None, - **kwargs, # pylint: disable=unused-argument - ) -> CryptographyClient: - """Gets a :class:`~azure.keyvault.keys.crypto.aio.CryptographyClient` for the given key. - - :param str key_name: The name of the key used to perform cryptographic operations. - - :keyword key_version: Optional version of the key used to perform cryptographic operations. - :paramtype key_version: str or None - - :returns: A :class:`~azure.keyvault.keys.crypto.aio.CryptographyClient` using the same options, credentials, and - HTTP client as this :class:`~azure.keyvault.keys.aio.KeyClient`. - :rtype: ~azure.keyvault.keys.crypto.aio.CryptographyClient - """ - key_id = _get_key_id(self._vault_url, key_name, key_version) - - # We provide a fake credential because the generated client already has the KeyClient's real credential - return CryptographyClient( - key_id, object(), generated_client=self._client, generated_models=self._models # type: ignore - ) - - @distributed_trace_async - async def create_key( - self, - name: str, - key_type: Union[str, KeyType], - *, - size: Optional[int] = None, - curve: Optional[Union[str, KeyCurveName]] = None, - public_exponent: Optional[int] = None, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a key or, if ``name`` is already in use, create a new version of the key. - - Requires keys/create permission. - - :param str name: The name of the new key. - :param key_type: The type of key to create - :type key_type: ~azure.keyvault.keys.KeyType or str - - :keyword size: Key size in bits. Applies only to RSA and symmetric keys. Consider using - :func:`create_rsa_key` or :func:`create_oct_key` instead. - :paramtype size: int or None - :keyword curve: Elliptic curve name. Applies only to elliptic curve keys. Defaults to the NIST P-256 - elliptic curve. To create an elliptic curve key, consider using :func:`create_ec_key` instead. - :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None - :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. - :paramtype public_exponent: int or None - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START create_key] - :end-before: [END create_key] - :language: python - :caption: Create a key - :dedent: 8 - """ - attributes = self._get_attributes( - enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable - ) - - policy = release_policy - if policy is not None: - policy = self._models.KeyReleasePolicy( - encoded_policy=policy.encoded_policy, content_type=policy.content_type, immutable=policy.immutable - ) - parameters = self._models.KeyCreateParameters( - kty=key_type, - key_size=size, - key_attributes=attributes, - key_ops=key_operations, - tags=tags, - curve=curve, - public_exponent=public_exponent, - release_policy=policy, - ) - - bundle = await self._client.create_key( - key_name=name, - parameters=parameters, - **kwargs, - ) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace_async - async def create_rsa_key( - self, - name: str, - *, - size: Optional[int] = None, - public_exponent: Optional[int] = None, - hardware_protected: Optional[bool] = False, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a new RSA key or, if ``name`` is already in use, create a new version of the key - - Requires the keys/create permission. - - :param str name: The name for the new key. - - :keyword size: Key size in bits, for example 2048, 3072, or 4096. - :paramtype size: int or None - :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. - :paramtype public_exponent: int or None - :keyword hardware_protected: Whether the key should be created in a hardware security module. - Defaults to ``False``. - :paramtype hardware_protected: bool or None - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START create_rsa_key] - :end-before: [END create_rsa_key] - :language: python - :caption: Create RSA key - :dedent: 8 - """ - return await self.create_key( - name, - key_type="RSA-HSM" if hardware_protected else "RSA", - size=size, - public_exponent=public_exponent, - key_operations=key_operations, - enabled=enabled, - tags=tags, - not_before=not_before, - expires_on=expires_on, - exportable=exportable, - release_policy=release_policy, - **kwargs, - ) - - @distributed_trace_async - async def create_ec_key( - self, - name: str, - *, - curve: Optional[Union[str, KeyCurveName]] = None, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - hardware_protected: Optional[bool] = False, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a new elliptic curve key or, if ``name`` is already in use, create a new version of the key. - - Requires the keys/create permission. - - :param str name: The name for the new key. - - :keyword curve: Elliptic curve name. Defaults to the NIST P-256 elliptic curve. - :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword hardware_protected: Whether the key should be created in a hardware security module. - Defaults to ``False``. - :paramtype hardware_protected: bool or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START create_ec_key] - :end-before: [END create_ec_key] - :language: python - :caption: Create an elliptic curve key - :dedent: 8 - """ - return await self.create_key( - name, - key_type="EC-HSM" if hardware_protected else "EC", - curve=curve, - key_operations=key_operations, - enabled=enabled, - tags=tags, - not_before=not_before, - expires_on=expires_on, - exportable=exportable, - release_policy=release_policy, - **kwargs, - ) - - @distributed_trace_async - async def create_oct_key( - self, - name: str, - *, - size: Optional[int] = None, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - hardware_protected: Optional[bool] = False, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Create a new octet sequence (symmetric) key or, if ``name`` is in use, create a new version of the key. - - Requires the keys/create permission. - - :param str name: The name for the new key. - - :keyword size: Key size in bits, for example 128, 192, or 256. - :paramtype size: int or None - :keyword key_operations: Allowed key operations. - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword hardware_protected: Whether the key should be created in a hardware security module. - Defaults to ``False``. - :paramtype hardware_protected: bool or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The created key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START create_oct_key] - :end-before: [END create_oct_key] - :language: python - :caption: Create an octet sequence (symmetric) key - :dedent: 8 - """ - return await self.create_key( - name, - key_type="oct-HSM" if hardware_protected else "oct", - size=size, - key_operations=key_operations, - enabled=enabled, - tags=tags, - not_before=not_before, - expires_on=expires_on, - exportable=exportable, - release_policy=release_policy, - **kwargs, - ) - - @distributed_trace_async - async def delete_key(self, name: str, **kwargs: Any) -> DeletedKey: - """Delete all versions of a key and its cryptographic material. - - Requires keys/delete permission. If the vault has soft-delete enabled, deletion may take several seconds to - complete. - - :param str name: The name of the key to delete - - :returns: The deleted key - :rtype: ~azure.keyvault.keys.DeletedKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START delete_key] - :end-before: [END delete_key] - :language: python - :caption: Delete a key - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, deleted_key_bundle = await self._client.delete_key( - key_name=name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - deleted_key = DeletedKey._from_deleted_key_bundle(deleted_key_bundle) - - polling_method = AsyncDeleteRecoverPollingMethod( - # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished - finished=deleted_key.recovery_id is None, - pipeline_response=pipeline_response, - command=partial(self.get_deleted_key, name=name, **kwargs), - final_resource=deleted_key, - interval=polling_interval, - ) - await polling_method.run() - - return polling_method.resource() - - @distributed_trace_async - async def get_key(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: - """Get a key's attributes and, if it's an asymmetric key, its public material. - - Requires keys/get permission. - - :param str name: The name of the key to get. - :param version: (optional) A specific version of the key to get. If not specified, gets the latest version - of the key. - :type version: str or None - - :returns: The fetched key. - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START get_key] - :end-before: [END get_key] - :language: python - :caption: Get a key - :dedent: 8 - """ - if version is None: - version = "" - - bundle = await self._client.get_key(name, version, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace_async - async def get_deleted_key(self, name: str, **kwargs: Any) -> DeletedKey: - """Get a deleted key. Possible only in a vault with soft-delete enabled. - - Requires keys/get permission. - - :param str name: The name of the key - - :returns: The deleted key - :rtype: ~azure.keyvault.keys.DeletedKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START get_deleted_key] - :end-before: [END get_deleted_key] - :language: python - :caption: Get a deleted key - :dedent: 8 - """ - bundle = await self._client.get_deleted_key(name, **kwargs) - return DeletedKey._from_deleted_key_bundle(bundle) - - @distributed_trace - def list_deleted_keys(self, **kwargs: Any) -> AsyncItemPaged[DeletedKey]: - """List all deleted keys, including the public part of each. Possible only in a vault with soft-delete enabled. - - Requires keys/list permission. - - :returns: An iterator of deleted keys - :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.keyvault.keys.DeletedKey] - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START list_deleted_keys] - :end-before: [END list_deleted_keys] - :language: python - :caption: List all the deleted keys - :dedent: 8 - """ - return self._client.get_deleted_keys( - maxresults=kwargs.pop("max_page_size", None), - cls=lambda objs: [DeletedKey._from_deleted_key_item(x) for x in objs], - **kwargs, - ) - - @distributed_trace - def list_properties_of_keys(self, **kwargs: Any) -> AsyncItemPaged[KeyProperties]: - """List identifiers and properties of all keys in the vault. - - Requires keys/list permission. - - :returns: An iterator of keys without their cryptographic material or version information - :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.keyvault.keys.KeyProperties] - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START list_keys] - :end-before: [END list_keys] - :language: python - :caption: List all keys - :dedent: 8 - """ - return self._client.get_keys( - maxresults=kwargs.pop("max_page_size", None), - cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], - **kwargs, - ) - - @distributed_trace - def list_properties_of_key_versions(self, name: str, **kwargs: Any) -> AsyncItemPaged[KeyProperties]: - """List the identifiers and properties of a key's versions. - - Requires keys/list permission. - - :param str name: The name of the key - - :returns: An iterator of keys without their cryptographic material - :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.keyvault.keys.KeyProperties] - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START list_properties_of_key_versions] - :end-before: [END list_properties_of_key_versions] - :language: python - :caption: List all versions of a key - :dedent: 8 - """ - return self._client.get_key_versions( - name, - maxresults=kwargs.pop("max_page_size", None), - cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], - **kwargs, - ) - - @distributed_trace_async - async def purge_deleted_key(self, name: str, **kwargs: Any) -> None: - """Permanently deletes a deleted key. Only possible in a vault with soft-delete enabled. - - Performs an irreversible deletion of the specified key, without possibility for recovery. The operation is not - available if the :py:attr:`~azure.keyvault.keys.KeyProperties.recovery_level` does not specify 'Purgeable'. - This method is only necessary for purging a key before its - :py:attr:`~azure.keyvault.keys.DeletedKey.scheduled_purge_date`. - - Requires keys/purge permission. - - :param str name: The name of the deleted key to purge - - :returns: None - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. code-block:: python - - # if the vault has soft-delete enabled, purge permanently deletes a deleted key - # (with soft-delete disabled, delete_key is permanent) - await key_client.purge_deleted_key("key-name") - - """ - await self._client.purge_deleted_key(name, **kwargs) - - @distributed_trace_async - async def recover_deleted_key(self, name: str, **kwargs: Any) -> KeyVaultKey: - """Recover a deleted key to its latest version. Possible only in a vault with soft-delete enabled. - - Requires keys/recover permission. If the vault does not have soft-delete enabled, :func:`delete_key` is - permanent, and this method will raise an error. Attempting to recover a non-deleted key will also raise an - error. - - :param str name: The name of the deleted key - - :returns: The recovered key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START recover_deleted_key] - :end-before: [END recover_deleted_key] - :language: python - :caption: Recover a deleted key - :dedent: 8 - """ - polling_interval = kwargs.pop("_polling_interval", None) - if polling_interval is None: - polling_interval = 2 - pipeline_response, recovered_key_bundle = await self._client.recover_deleted_key( - key_name=name, - cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), - **kwargs, - ) - recovered_key = KeyVaultKey._from_key_bundle(recovered_key_bundle) - - command = partial(self.get_key, name=name, **kwargs) - polling_method = AsyncDeleteRecoverPollingMethod( - pipeline_response=pipeline_response, - command=command, - final_resource=recovered_key, - finished=False, - interval=polling_interval, - ) - await polling_method.run() - - return polling_method.resource() - - @distributed_trace_async - async def update_key_properties( - self, - name: str, - version: Optional[str] = None, - *, - key_operations: Optional[List[Union[str, KeyOperation]]] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Change a key's properties (not its cryptographic material). - - Requires keys/update permission. - - :param str name: The name of key to update - :param version: (optional) The version of the key to update. If unspecified, the latest version is updated. - :type version: str or None - - :keyword key_operations: Allowed key operations - :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The updated key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START update_key] - :end-before: [END update_key] - :language: python - :caption: Update a key's attributes - :dedent: 8 - """ - attributes = self._get_attributes(enabled=enabled, not_before=not_before, expires_on=expires_on) - - policy = release_policy - if policy is not None: - policy = self._models.KeyReleasePolicy( - content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable - ) - parameters = self._models.KeyUpdateParameters( - key_ops=key_operations, - key_attributes=attributes, - tags=tags, - release_policy=policy, - ) - - bundle = await self._client.update_key( - name, - key_version=version or "", - parameters=parameters, - **kwargs, - ) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace_async - async def backup_key(self, name: str, **kwargs: Any) -> bytes: - """Back up a key in a protected form useable only by Azure Key Vault. - - Requires key/backup permission. This is intended to allow copying a key from one vault to another. Both vaults - must be owned by the same Azure subscription. Also, backup / restore cannot be performed across geopolitical - boundaries. For example, a backup from a vault in a USA region cannot be restored to a vault in an EU region. - - :param str name: The name of the key to back up - - :returns: The key backup result, in a protected bytes format that can only be used by Azure Key Vault. - :rtype: bytes - - :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: - the former if the key doesn't exist; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START backup_key] - :end-before: [END backup_key] - :language: python - :caption: Get a key backup - :dedent: 8 - """ - backup_result = await self._client.backup_key(name, **kwargs) - return backup_result.value - - @distributed_trace_async - async def restore_key_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultKey: - """Restore a key backup to the vault. - - Requires keys/restore permission. This imports all versions of the key, with its name, attributes, and access - control policies. If the key's name is already in use, restoring it will fail. Also, the target vault must be - owned by the same Microsoft Azure subscription as the source vault. - - :param bytes backup: A key backup as returned by :func:`backup_key` - - :returns: The restored key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.ResourceExistsError or ~azure.core.exceptions.HttpResponseError: - the former if the backed up key's name is already in use; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_samples_keys_async.py - :start-after: [START restore_key_backup] - :end-before: [END restore_key_backup] - :language: python - :caption: Restore a key backup - :dedent: 8 - """ - bundle = await self._client.restore_key( - parameters=self._models.KeyRestoreParameters(key_bundle_backup=backup), - **kwargs, - ) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace_async - async def import_key( - self, - name: str, - key: JsonWebKey, - *, - hardware_protected: Optional[bool] = None, - enabled: Optional[bool] = None, - tags: Optional[Dict[str, str]] = None, - not_before: Optional[datetime] = None, - expires_on: Optional[datetime] = None, - exportable: Optional[bool] = None, - release_policy: Optional[KeyReleasePolicy] = None, - **kwargs: Any, - ) -> KeyVaultKey: - """Import a key created externally. - - Requires keys/import permission. If ``name`` is already in use, the key will be imported as a new version. - - :param str name: Name for the imported key - :param key: The JSON web key to import - :type key: ~azure.keyvault.keys.JsonWebKey - - :keyword hardware_protected: Whether the key should be backed by a hardware security module - :paramtype hardware_protected: bool or None - :keyword enabled: Whether the key is enabled for use. - :paramtype enabled: bool or None - :keyword tags: Application specific metadata in the form of key-value pairs. - :paramtype tags: dict[str, str] or None - :keyword not_before: Not before date of the key in UTC - :paramtype not_before: ~datetime.datetime or None - :keyword expires_on: Expiry date of the key in UTC - :paramtype expires_on: ~datetime.datetime or None - :keyword exportable: Whether the private key can be exported. - :paramtype exportable: bool or None - :keyword release_policy: The policy rules under which the key can be exported. - :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None - - :returns: The imported key - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - """ - attributes = self._get_attributes( - enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable - ) - - policy = release_policy - if policy is not None: - policy = self._models.KeyReleasePolicy( - content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable - ) - parameters = self._models.KeyImportParameters( - key=key._to_generated_model(), - key_attributes=attributes, - hsm=hardware_protected, - tags=tags, - release_policy=policy, - ) - - bundle = await self._client.import_key(name, parameters=parameters, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace_async - async def release_key( - self, - name: str, - target_attestation_token: str, - *, - version: Optional[str] = None, - algorithm: Optional[Union[str, KeyExportEncryptionAlgorithm]] = None, - nonce: Optional[str] = None, - **kwargs: Any, - ) -> ReleaseKeyResult: - """Releases a key. - - The release key operation is applicable to all key types. The target key must be marked - exportable. This operation requires the keys/release permission. - - :param str name: The name of the key to get. - :param str target_attestation_token: The attestation assertion for the target of the key release. - - :keyword version: A specific version of the key to release. If unspecified, the latest version is released. - :paramtype version: str or None - :keyword algorithm: The encryption algorithm to use to protect the released key material. - :paramtype algorithm: str or ~azure.keyvault.keys.KeyExportEncryptionAlgorithm or None - :keyword nonce: A client-provided nonce for freshness. - :paramtype nonce: str or None - - :return: The result of the key release. - :rtype: ~azure.keyvault.keys.ReleaseKeyResult - - :raises ~azure.core.exceptions.HttpResponseError: - """ - result = await self._client.release( - key_name=name, - key_version=version or "", - parameters=self._models.KeyReleaseParameters( - target_attestation_token=target_attestation_token, - nonce=nonce, - enc=algorithm, - ), - **kwargs, - ) - return ReleaseKeyResult(result.value) - - @distributed_trace_async - async def get_random_bytes(self, count: int, **kwargs: Any) -> bytes: - """Get the requested number of random bytes from a managed HSM. - - :param int count: The requested number of random bytes. - - :return: The random bytes. - :rtype: bytes - - :raises ValueError or ~azure.core.exceptions.HttpResponseError: - the former if less than one random byte is requested; the latter for other errors - - Example: - .. literalinclude:: ../tests/test_keys_async.py - :start-after: [START get_random_bytes] - :end-before: [END get_random_bytes] - :language: python - :caption: Get random bytes - :dedent: 12 - """ - if count < 1: - raise ValueError("At least one random byte must be requested") - parameters = self._models.GetRandomBytesRequest(count=count) - result = await self._client.get_random_bytes(parameters=parameters, **kwargs) - return result.value - - @distributed_trace_async - async def get_key_rotation_policy(self, key_name: str, **kwargs: Any) -> KeyRotationPolicy: - """Get the rotation policy of a Key Vault key. - - :param str key_name: The name of the key. - - :return: The key rotation policy. - :rtype: ~azure.keyvault.keys.KeyRotationPolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - policy = await self._client.get_key_rotation_policy(key_name=key_name, **kwargs) - return KeyRotationPolicy._from_generated(policy) - - @distributed_trace_async - async def rotate_key(self, name: str, **kwargs: Any) -> KeyVaultKey: - """Rotate the key based on the key policy by generating a new version of the key. - - This operation requires the keys/rotate permission. - - :param str name: The name of the key to rotate. - - :return: The new version of the rotated key. - :rtype: ~azure.keyvault.keys.KeyVaultKey - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = await self._client.rotate_key(key_name=name, **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - @distributed_trace_async - async def update_key_rotation_policy( # pylint: disable=unused-argument - self, - key_name: str, - policy: KeyRotationPolicy, - *, - lifetime_actions: Optional[List[KeyRotationLifetimeAction]] = None, - expires_in: Optional[str] = None, - **kwargs: Any, - ) -> KeyRotationPolicy: - """Updates the rotation policy of a Key Vault key. - - This operation requires the keys/update permission. - - :param str key_name: The name of the key in the given vault. - :param policy: The new rotation policy for the key. - :type policy: ~azure.keyvault.keys.KeyRotationPolicy - - :keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key. This will - override the lifetime actions of the provided ``policy``. - :paramtype lifetime_actions: List[~azure.keyvault.keys.KeyRotationLifetimeAction] - :keyword str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an - ISO 8601 duration. For example: 90 days is "P90D", 3 months is "P3M", and 48 hours is "PT48H". See - `Wikipedia `_ for more information on ISO 8601 durations. - This will override the expiry time of the provided ``policy``. - - :return: The updated rotation policy. - :rtype: ~azure.keyvault.keys.KeyRotationPolicy - - :raises ~azure.core.exceptions.HttpResponseError: - """ - actions = lifetime_actions or policy.lifetime_actions - if actions: - actions = [ - self._models.LifetimeActions( - action=self._models.LifetimeActionsType(type=action.action), - trigger=self._models.LifetimeActionsTrigger( - time_after_create=action.time_after_create, time_before_expiry=action.time_before_expiry - ), - ) - for action in actions - ] - - attributes = self._models.KeyRotationPolicyAttributes(expiry_time=expires_in or policy.expires_in) - new_policy = self._models.KeyRotationPolicy(lifetime_actions=actions or [], attributes=attributes) - result = await self._client.update_key_rotation_policy(key_name=key_name, key_rotation_policy=new_policy) - return KeyRotationPolicy._from_generated(result) - - @distributed_trace_async - async def get_key_attestation(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: - """Get a key and its attestation blob. - - This method is applicable to any key stored in Azure Key Vault Managed HSM. This operation requires the keys/get - permission. - - :param str name: The name of the key. - :param version: (optional) A specific version of the key to get. If not specified, gets the latest version - of the key. - :type version: str or None - - :return: The key attestation. - :rtype: ~azure.keyvault.keys.KeyAttestation - - :raises ~azure.core.exceptions.HttpResponseError: - """ - bundle = await self._client.get_key_attestation(key_name=name, key_version=version or "", **kwargs) - return KeyVaultKey._from_key_bundle(bundle) - - async def __aenter__(self) -> "KeyClient": - await self._client.__aenter__() - return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py deleted file mode 100644 index 9e931898fc8e..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from ._models import ( - DecryptResult, - EncryptResult, - KeyVaultRSAPrivateKey, - KeyVaultRSAPublicKey, - SignResult, - WrapResult, - VerifyResult, - UnwrapResult, -) -from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm -from ._client import CryptographyClient - - -__all__ = [ - "CryptographyClient", - "DecryptResult", - "EncryptionAlgorithm", - "EncryptResult", - "KeyVaultRSAPrivateKey", - "KeyVaultRSAPublicKey", - "KeyWrapAlgorithm", - "SignatureAlgorithm", - "SignResult", - "WrapResult", - "VerifyResult", - "UnwrapResult", -] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py deleted file mode 100644 index 51f273d8f858..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py +++ /dev/null @@ -1,577 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from datetime import datetime -import logging -from typing import Any, cast, Dict, Optional, Union - -from azure.core.credentials import TokenCredential -from azure.core.exceptions import HttpResponseError -from azure.core.tracing.decorator import distributed_trace - -from . import ( - DecryptResult, - EncryptionAlgorithm, - EncryptResult, - KeyWrapAlgorithm, - SignatureAlgorithm, - SignResult, - VerifyResult, - UnwrapResult, - WrapResult, -) -from ._key_validity import raise_if_time_invalid -from ._models import KeyVaultRSAPrivateKey, KeyVaultRSAPublicKey -from ._providers import get_local_cryptography_provider, NoLocalCryptography -from .. import KeyOperation -from .._models import JsonWebKey, KeyVaultKey -from .._shared import KeyVaultClientBase, KeyVaultResourceId, parse_key_vault_id - -_LOGGER = logging.getLogger(__name__) - - -def _validate_arguments( - operation: KeyOperation, - algorithm: EncryptionAlgorithm, - *, - iv: Optional[bytes] = None, - tag: Optional[bytes] = None, - aad: Optional[bytes] = None, -) -> None: - """Validates the arguments passed to perform an operation with a provided algorithm. - - :param KeyOperation operation: the type of operation being requested - :param EncryptionAlgorithm algorithm: the encryption algorithm to use for the operation - - :keyword iv: initialization vector - :paramtype iv: bytes or None - :keyword tag: authentication tag returned from an encryption - :paramtype tag: bytes or None - :keyword aad: data that is authenticated but not encrypted - :paramtype aad: bytes or None - - :raises ValueError: if parameters that are incompatible with the specified algorithm are provided. - """ - if operation == KeyOperation.encrypt: - if iv and "CBC" not in algorithm: - raise ValueError(f"iv should only be provided with AES-CBC algorithms; {algorithm} does not accept an iv") - if iv is None and "CBC" in algorithm: - raise ValueError("iv is a required parameter for encryption with AES-CBC algorithms.") - if aad and not ("CBC" in algorithm or "GCM" in algorithm): - raise ValueError( - f"additional_authenticated_data should only be provided with AES algorithms; {algorithm} does not " - "accept additional authenticated data" - ) - - if operation == KeyOperation.decrypt: - if iv and not ("CBC" in algorithm or "GCM" in algorithm): - raise ValueError(f"iv should only be provided with AES algorithms; {algorithm} does not accept an iv") - if iv is None and ("CBC" in algorithm or "GCM" in algorithm): - raise ValueError("iv is a required parameter for decryption with AES algorithms.") - if tag and "GCM" not in algorithm: - raise ValueError( - f"authentication_tag should only be provided with AES-GCM algorithms; {algorithm} does not accept a tag" - ) - if tag is None and "GCM" in algorithm: - raise ValueError("authentication_tag is a required parameter for AES-GCM decryption.") - if aad and not ("CBC" in algorithm or "GCM" in algorithm): - raise ValueError( - f"additional_authenticated_data should only be provided with AES algorithms; {algorithm} does not " - "accept additional authenticated data" - ) - - -class CryptographyClient(KeyVaultClientBase): - """Performs cryptographic operations using Azure Key Vault keys. - - This client will perform operations locally when it's intialized with the necessary key material or is able to get - that material from Key Vault. When the required key material is unavailable, cryptographic operations are performed - by the Key Vault service. - - :param key: Either a azure.keyvault.keys.KeyVaultKey instance as returned by - :func:`~azure.keyvault.keys.KeyClient.get_key`, or a string. - If a string, the value must be the identifier of an Azure Key Vault key. Including a version is recommended. - :type key: str or azure.keyvault.keys.KeyVaultKey - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity` - :type credential: ~azure.core.credentials.TokenCredential - - :keyword api_version: Version of the service API to use. Defaults to the most recent. - :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str - :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key - Vault or Managed HSM domain. Defaults to True. - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START create_client] - :end-before: [END create_client] - :caption: Create a CryptographyClient - :language: python - :dedent: 8 - """ - - # pylint:disable=protected-access - - def __init__(self, key: Union[KeyVaultKey, str], credential: TokenCredential, **kwargs: Any) -> None: - self._jwk = kwargs.pop("_jwk", False) - self._not_before: Optional[datetime] = None - self._expires_on: Optional[datetime] = None - self._key_id: Optional[KeyVaultResourceId] = None - - if isinstance(key, KeyVaultKey): - self._key: Union[JsonWebKey, KeyVaultKey, str, None] = key.key - self._key_id = parse_key_vault_id(key.id) - if key.properties._attributes: - self._not_before = key.properties.not_before - self._expires_on = key.properties.expires_on - elif isinstance(key, str): - self._key = None - self._key_id = parse_key_vault_id(key) - if self._key_id.version is None: - self._key_id.version = "" # to avoid an error and get the latest version when getting the key - self._keys_get_forbidden = False - elif self._jwk: - self._key = key - else: - raise ValueError("'key' must be a KeyVaultKey instance or a key ID string") - - if self._jwk: - try: - self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) - self._initialized = True - except Exception as ex: - raise ValueError("The provided jwk is not valid for local cryptography") from ex - else: - self._local_provider = NoLocalCryptography() - self._initialized = False - - self._vault_url = None if (self._jwk or self._key_id is None) else self._key_id.vault_url # type: ignore - super(CryptographyClient, self).__init__( - vault_url=self._vault_url or "vault_url", credential=credential, **kwargs - ) - - @property - def key_id(self) -> Optional[str]: - """The full identifier of the client's key. - - This property may be None when a client is constructed with :func:`from_jwk`. - - :returns: The full identifier of the client's key. - :rtype: str or None - """ - if not self._jwk: - return self._key_id.source_id if self._key_id else None - return cast(JsonWebKey, self._key).kid # type: ignore[attr-defined] - - @property - def vault_url(self) -> Optional[str]: # type: ignore - """The base vault URL of the client's key. - - This property may be None when a client is constructed with :func:`from_jwk`. - - :returns: The base vault URL of the client's key. - :rtype: str or None - """ - return self._vault_url - - @classmethod - def from_jwk(cls, jwk: Union[JsonWebKey, Dict[str, Any]]) -> "CryptographyClient": - """Creates a client that can only perform cryptographic operations locally. - - :param jwk: the key's cryptographic material, as a JsonWebKey or dictionary. - :type jwk: JsonWebKey or Dict[str, Any] - - :returns: A client that can only perform local cryptographic operations. - :rtype: CryptographyClient - """ - if not isinstance(jwk, JsonWebKey): - jwk = JsonWebKey(**jwk) - return cls(jwk, object(), _jwk=True) # type: ignore - - @distributed_trace - def _initialize(self, **kwargs: Any) -> None: - if self._initialized: - return - - # try to get the key material, if we don't have it and aren't forbidden to do so - if not (self._key or self._keys_get_forbidden): - try: - key_bundle = self._client.get_key( - self._key_id.name if self._key_id else None, - self._key_id.version if self._key_id else None, - **kwargs, - ) - key = KeyVaultKey._from_key_bundle(key_bundle) - self._key = key.key - self._key_id = parse_key_vault_id(key.id) # update the key ID in case we didn't have the version before - except HttpResponseError as ex: - # if we got a 403, we don't have keys/get permission and won't try to get the key again - # (other errors may be transient) - self._keys_get_forbidden = ex.status_code == 403 - - # if we have the key material, create a local crypto provider with it - if self._key: - self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) - self._initialized = True - else: - # try to get the key again next time unless we know we're forbidden to do so - self._initialized = self._keys_get_forbidden - - @distributed_trace - def create_rsa_private_key(self) -> KeyVaultRSAPrivateKey: # pylint:disable=client-method-missing-kwargs - """Create an `RSAPrivateKey` implementation backed by this `CryptographyClient`, as a `KeyVaultRSAPrivateKey`. - - The `CryptographyClient` will attempt to download the key, if it hasn't been already, as part of this operation. - - :returns: A `KeyVaultRSAPrivateKey`, which implements `cryptography`'s `RSAPrivateKey` interface. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey - """ - self._initialize() - return KeyVaultRSAPrivateKey(client=self, key_material=cast(JsonWebKey, self._key)) - - @distributed_trace - def create_rsa_public_key(self) -> KeyVaultRSAPublicKey: # pylint:disable=client-method-missing-kwargs - """Create an `RSAPublicKey` implementation backed by this `CryptographyClient`, as a `KeyVaultRSAPublicKey`. - - The `CryptographyClient` will attempt to download the key, if it hasn't been already, as part of this operation. - - :returns: A `KeyVaultRSAPublicKey`, which implements `cryptography`'s `RSAPublicKey` interface. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey - """ - self._initialize() - return KeyVaultRSAPublicKey(client=self, key_material=cast(JsonWebKey, self._key)) - - @distributed_trace - def encrypt( - self, - algorithm: EncryptionAlgorithm, - plaintext: bytes, - *, - iv: Optional[bytes] = None, - additional_authenticated_data: Optional[bytes] = None, - **kwargs: Any, - ) -> EncryptResult: - """Encrypt bytes using the client's key. - - Requires the keys/encrypt permission. This method encrypts only a single block of data, whose size depends on - the key and encryption algorithm. - - :param algorithm: Encryption algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm - :param bytes plaintext: Bytes to encrypt - - :keyword iv: Initialization vector. Required for only AES-CBC(PAD) encryption. If you pass your own IV, - make sure you use a cryptographically random, non-repeating IV. If omitted, an attempt will be made to - generate an IV via `os.urandom `_ for local - cryptography; for remote cryptography, Key Vault will generate an IV. - :paramtype iv: bytes or None - :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use - with AES-GCM encryption. - :paramtype additional_authenticated_data: bytes or None - - :returns: The result of the encryption operation. - :rtype: ~azure.keyvault.keys.crypto.EncryptResult - - :raises ValueError: if parameters that are incompatible with the specified algorithm are provided, or if - generating an IV fails on the current platform. - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START encrypt] - :end-before: [END encrypt] - :caption: Encrypt bytes - :language: python - :dedent: 8 - """ - _validate_arguments( - operation=KeyOperation.encrypt, algorithm=algorithm, iv=iv, aad=additional_authenticated_data - ) - self._initialize(**kwargs) - - if self._local_provider.supports(KeyOperation.encrypt, algorithm): - raise_if_time_invalid(self._not_before, self._expires_on) - try: - return self._local_provider.encrypt(algorithm, plaintext, iv=iv) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local encrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.encrypt}" operation with algorithm "{algorithm}"' - ) - - operation_result = self._client.encrypt( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters( - algorithm=algorithm, value=plaintext, iv=iv, aad=additional_authenticated_data - ), - **kwargs, - ) - - result_iv = operation_result.iv if hasattr(operation_result, "iv") else None - result_tag = operation_result.authentication_tag if hasattr(operation_result, "authentication_tag") else None - result_aad = ( - operation_result.additional_authenticated_data - if hasattr(operation_result, "additional_authenticated_data") - else None - ) - - return EncryptResult( - key_id=self.key_id, - algorithm=algorithm, - ciphertext=operation_result.result, - iv=result_iv, - authentication_tag=result_tag, - additional_authenticated_data=result_aad, - ) - - @distributed_trace - def decrypt( - self, - algorithm: EncryptionAlgorithm, - ciphertext: bytes, - *, - iv: Optional[bytes] = None, - authentication_tag: Optional[bytes] = None, - additional_authenticated_data: Optional[bytes] = None, - **kwargs: Any, - ) -> DecryptResult: - """Decrypt a single block of encrypted data using the client's key. - - Requires the keys/decrypt permission. This method decrypts only a single block of data, whose size depends on - the key and encryption algorithm. - - :param algorithm: Encryption algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm - :param bytes ciphertext: Encrypted bytes to decrypt. Microsoft recommends you not use CBC without first ensuring - the integrity of the ciphertext using, for example, an HMAC. See - https://learn.microsoft.com/dotnet/standard/security/vulnerabilities-cbc-mode for more information. - - :keyword iv: The initialization vector used during encryption. Required for AES decryption. - :paramtype iv: bytes or None - :keyword authentication_tag: The authentication tag generated during encryption. Required for only AES-GCM - decryption. - :paramtype authentication_tag: bytes or None - :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use - with AES-GCM decryption. - :paramtype additional_authenticated_data: bytes or None - - :returns: The result of the decryption operation. - :rtype: ~azure.keyvault.keys.crypto.DecryptResult - - :raises ValueError: If parameters that are incompatible with the specified algorithm are provided. - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START decrypt] - :end-before: [END decrypt] - :caption: Decrypt bytes - :language: python - :dedent: 8 - """ - _validate_arguments( - operation=KeyOperation.decrypt, - algorithm=algorithm, - iv=iv, - tag=authentication_tag, - aad=additional_authenticated_data, - ) - self._initialize(**kwargs) - - if self._local_provider.supports(KeyOperation.decrypt, algorithm): - try: - return self._local_provider.decrypt(algorithm, ciphertext, iv=iv) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local decrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.decrypt}" operation with algorithm "{algorithm}"' - ) - - operation_result = self._client.decrypt( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters( - algorithm=algorithm, value=ciphertext, iv=iv, tag=authentication_tag, aad=additional_authenticated_data - ), - **kwargs, - ) - - return DecryptResult(key_id=self.key_id, algorithm=algorithm, plaintext=operation_result.result) - - @distributed_trace - def wrap_key(self, algorithm: KeyWrapAlgorithm, key: bytes, **kwargs: Any) -> WrapResult: - """Wrap a key with the client's key. - - Requires the keys/wrapKey permission. - - :param algorithm: wrapping algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm - :param bytes key: key to wrap - - :returns: The result of the wrapping operation. - :rtype: ~azure.keyvault.keys.crypto.WrapResult - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START wrap_key] - :end-before: [END wrap_key] - :caption: Wrap a key - :language: python - :dedent: 8 - """ - self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.wrap_key, algorithm): - raise_if_time_invalid(self._not_before, self._expires_on) - try: - return self._local_provider.wrap_key(algorithm, key) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local wrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.wrap_key}" operation with algorithm "{algorithm}"' - ) - - operation_result = self._client.wrap_key( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=key), - **kwargs, - ) - - return WrapResult(key_id=self.key_id, algorithm=algorithm, encrypted_key=operation_result.result) - - @distributed_trace - def unwrap_key(self, algorithm: KeyWrapAlgorithm, encrypted_key: bytes, **kwargs: Any) -> UnwrapResult: - """Unwrap a key previously wrapped with the client's key. - - Requires the keys/unwrapKey permission. - - :param algorithm: wrapping algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm - :param bytes encrypted_key: the wrapped key - - :returns: The result of the unwrapping operation. - :rtype: ~azure.keyvault.keys.crypto.UnwrapResult - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START unwrap_key] - :end-before: [END unwrap_key] - :caption: Unwrap a key - :language: python - :dedent: 8 - """ - self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.unwrap_key, algorithm): - try: - return self._local_provider.unwrap_key(algorithm, encrypted_key) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local unwrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.unwrap_key}" operation with algorithm "{algorithm}"' - ) - - operation_result = self._client.unwrap_key( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=encrypted_key), - **kwargs, - ) - return UnwrapResult(key_id=self.key_id, algorithm=algorithm, key=operation_result.result) - - @distributed_trace - def sign(self, algorithm: SignatureAlgorithm, digest: bytes, **kwargs: Any) -> SignResult: - """Create a signature from a digest using the client's key. - - Requires the keys/sign permission. - - :param algorithm: signing algorithm - :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm - :param bytes digest: hashed bytes to sign - - :returns: The result of the signing operation. - :rtype: ~azure.keyvault.keys.crypto.SignResult - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START sign] - :end-before: [END sign] - :caption: Sign bytes - :language: python - :dedent: 8 - """ - self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.sign, algorithm): - raise_if_time_invalid(self._not_before, self._expires_on) - try: - return self._local_provider.sign(algorithm, digest) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local sign operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.sign}" operation with algorithm "{algorithm}"' - ) - - operation_result = self._client.sign( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeySignParameters(algorithm=algorithm, value=digest), - **kwargs, - ) - - return SignResult(key_id=self.key_id, algorithm=algorithm, signature=operation_result.result) - - @distributed_trace - def verify(self, algorithm: SignatureAlgorithm, digest: bytes, signature: bytes, **kwargs: Any) -> VerifyResult: - """Verify a signature using the client's key. - - Requires the keys/verify permission. - - :param algorithm: verification algorithm - :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm - :param bytes digest: Pre-hashed digest corresponding to **signature**. The hash algorithm used must be - compatible with ``algorithm``. - :param bytes signature: signature to verify - - :returns: The result of the verifying operation. - :rtype: ~azure.keyvault.keys.crypto.VerifyResult - - .. literalinclude:: ../tests/test_examples_crypto.py - :start-after: [START verify] - :end-before: [END verify] - :caption: Verify a signature - :language: python - :dedent: 8 - """ - self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.verify, algorithm): - try: - return self._local_provider.verify(algorithm, digest, signature) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local verify operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.verify}" operation with algorithm "{algorithm}"' - ) - - operation_result = self._client.verify( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyVerifyParameters(algorithm=algorithm, digest=digest, signature=signature), - **kwargs, - ) - - return VerifyResult(key_id=self.key_id, algorithm=algorithm, is_valid=operation_result.value) - - def __enter__(self) -> "CryptographyClient": - self._client.__enter__() - return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py deleted file mode 100644 index 356b72b5edc5..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py +++ /dev/null @@ -1,67 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from enum import Enum -from azure.core import CaseInsensitiveEnumMeta - - -# pylint: disable=enum-must-be-uppercase -class KeyWrapAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Key wrapping algorithms""" - - aes_128 = "A128KW" - aes_192 = "A192KW" - aes_256 = "A256KW" - # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. - # Microsoft does *not* recommend RSA_OAEP, which is included solely for backwards compatibility. - # RSA_OAEP utilizes SHA1, which has known collision problems. - rsa_oaep = "RSA-OAEP" - rsa_oaep_256 = "RSA-OAEP-256" - # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. - # Microsoft does *not* recommend RSA_1_5, which is included solely for backwards compatibility. - # Cryptographic standards no longer consider RSA with the PKCS#1 v1.5 padding scheme secure for encryption. - rsa1_5 = "RSA1_5" - ckm_aes_key_wrap = "CKM_AES_KEY_WRAP" - ckm_aes_key_wrap_pad = "CKM_AES_KEY_WRAP_PAD" - - -class EncryptionAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Encryption algorithms""" - - # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. - # Microsoft does *not* recommend RSA_OAEP, which is included solely for backwards compatibility. - # RSA_OAEP utilizes SHA1, which has known collision problems. - rsa_oaep = "RSA-OAEP" - rsa_oaep_256 = "RSA-OAEP-256" - # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. - # Microsoft does *not* recommend RSA_1_5, which is included solely for backwards compatibility. - # Cryptographic standards no longer consider RSA with the PKCS#1 v1.5 padding scheme secure for encryption. - rsa1_5 = "RSA1_5" - a128_gcm = "A128GCM" - a192_gcm = "A192GCM" - a256_gcm = "A256GCM" - a128_cbc = "A128CBC" - a192_cbc = "A192CBC" - a256_cbc = "A256CBC" - a128_cbcpad = "A128CBCPAD" - a192_cbcpad = "A192CBCPAD" - a256_cbcpad = "A256CBCPAD" - - -class SignatureAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): - """Signature algorithms, described in https://tools.ietf.org/html/rfc7518""" - - ps256 = "PS256" #: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 - ps384 = "PS384" #: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 - ps512 = "PS512" #: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 - rs256 = "RS256" #: RSASSA-PKCS1-v1_5 using SHA-256 - rs384 = "RS384" #: RSASSA-PKCS1-v1_5 using SHA-384 - rs512 = "RS512" #: RSASSA-PKCS1-v1_5 using SHA-512 - es256 = "ES256" #: ECDSA using P-256 and SHA-256 - es384 = "ES384" #: ECDSA using P-384 and SHA-384 - es512 = "ES512" #: ECDSA using P-521 and SHA-512 - es256_k = "ES256K" #: ECDSA using P-256K and SHA-256 - hs256 = "HS256" #: HMAC using SHA-256 - hs384 = "HS384" #: HMAC using SHA-384 - hs512 = "HS512" #: HMAC using SHA-512 diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py deleted file mode 100644 index 880d4cdeb7ae..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from .algorithm import ( - Algorithm, - AsymmetricEncryptionAlgorithm, - SymmetricEncryptionAlgorithm, - AuthenticatedSymmetricEncryptionAlgorithm, - SignatureAlgorithm, -) -from .ec_key import EllipticCurveKey -from .key import Key -from .rsa_key import RsaKey -from .symmetric_key import SymmetricKey -from .transform import CryptoTransform, BlockCryptoTransform, AuthenticatedCryptoTransform, SignatureTransform - -__all__ = [ - "Key", - "EllipticCurveKey", - "RsaKey", - "Algorithm", - "AsymmetricEncryptionAlgorithm", - "SymmetricEncryptionAlgorithm", - "AuthenticatedCryptoTransform", - "SignatureAlgorithm", - "CryptoTransform", - "BlockCryptoTransform", - "AuthenticatedSymmetricEncryptionAlgorithm", - "SignatureTransform", - "SymmetricKey", -] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py deleted file mode 100644 index f19eb2cb0ee0..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py +++ /dev/null @@ -1,131 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import codecs -from base64 import b64encode, b64decode - -from cryptography.hazmat.primitives.asymmetric import utils - - -def _bytes_to_int(b): - if not b or not isinstance(b, bytes): - raise ValueError("b must be non-empty byte string") - - return int(codecs.encode(b, "hex"), 16) - - -def _int_to_bytes(i): - h = hex(i) - if len(h) > 1 and h[0:2] == "0x": - h = h[2:] - - # need to strip L in python 2.x - h = h.strip("L") - - if len(h) % 2: - h = "0" + h - return codecs.decode(h, "hex") - - -def _bstr_to_b64url(bstr): - """Serialize bytes into base-64 string. - - :param bytes bstr: Object to be serialized. - - :returns: The base-64 URL encoded string. - :rtype: str - """ - encoded = b64encode(bstr).decode() - return encoded.strip("=").replace("+", "-").replace("/", "_") - - -def _str_to_b64url(s): - """Serialize str into base-64 string. - - :param str s: Object to be serialized. - - :returns: The base-64 URL encoded string. - :rtype: str - """ - return _bstr_to_b64url(s.encode(encoding="utf8")) - - -def _b64_to_bstr(b64str): - """Deserialize base-64 encoded string into string. - - :param str b64str: response string to be deserialized. - - :returns: The decoded bytes. - :rtype: bytes - - :raises: TypeError if string format invalid. - """ - padding = "=" * (3 - (len(b64str) + 3) % 4) - b64str = b64str + padding - encoded = b64str.replace("-", "+").replace("_", "/") - return b64decode(encoded) - - -def _b64_to_str(b64str): - """Deserialize base-64 encoded string into string. - - :param str b64str: response string to be deserialized. - - :returns: The decoded string. - :rtype: str - - :raises: TypeError if string format invalid. - """ - return _b64_to_bstr(b64str).decode("utf8") - - -def _int_to_fixed_length_bigendian_bytes(i, length): - """Convert an integer to a bigendian byte string left-padded with zeroes to a fixed length. - - :param int i: The integer to convert. - :param int length: The length of the desired byte string. - - :returns: A bigendian byte string of length `length`, representing integer `i`. - :rtype: bytes - """ - - b = _int_to_bytes(i) - - if len(b) > length: - raise ValueError(f"{i} is too large to be represented by {length} bytes") - - if len(b) < length: - b = (b"\0" * (length - len(b))) + b - - return b - - -def ecdsa_to_asn1_der(signature): - """ASN.1 DER encode an ECDSA signature. - - :param bytes signature: ECDSA signature encoded according to RFC 7518, i.e. the concatenated big-endian bytes of - two integers (as produced by Key Vault) - - :returns: signature, ASN.1 DER encoded (as expected by ``cryptography``) - :rtype: bytes - """ - mid = len(signature) // 2 - r = _bytes_to_int(signature[:mid]) - s = _bytes_to_int(signature[mid:]) - return utils.encode_dss_signature(r, s) - - -def asn1_der_to_ecdsa(signature, algorithm): - """Convert an ASN.1 DER encoded signature to ECDSA encoding. - - :param bytes signature: an ASN.1 DER encoded ECDSA signature (as produced by ``cryptography``) - :param _Ecdsa algorithm: signing algorithm which produced ``signature`` - - :returns: signature encoded according to RFC 7518 (as expected by Key Vault) - :rtype: bytes - """ - r, s = utils.decode_dss_signature(signature) - r_bytes = _int_to_fixed_length_bigendian_bytes(r, algorithm.coordinate_length) - s_bytes = _int_to_fixed_length_bigendian_bytes(s, algorithm.coordinate_length) - return r_bytes + s_bytes diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py deleted file mode 100644 index 1b2c2446ed6b..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py +++ /dev/null @@ -1,78 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from abc import abstractmethod -from typing import Optional, TYPE_CHECKING, Union - -if TYPE_CHECKING: - from cryptography.hazmat.primitives import hashes - - -_alg_registry = {} - - -class Algorithm(object): - _name: Optional[str] = None - - @classmethod - def name(cls): - return cls._name - - @classmethod - def register(cls): - _alg_registry[cls._name] = cls - - @staticmethod - def resolve(name): - if name not in _alg_registry: - return None - return _alg_registry[name]() - - -class AsymmetricEncryptionAlgorithm(Algorithm): - @abstractmethod - def create_encryptor(self, key): - raise NotImplementedError() - - @abstractmethod - def create_decryptor(self, key): - raise NotImplementedError() - - -class SymmetricEncryptionAlgorithm(Algorithm): - @abstractmethod - def create_encryptor(self, key, iv): - raise NotImplementedError() - - @abstractmethod - def create_decryptor(self, key, iv): - raise NotImplementedError() - - -class AuthenticatedSymmetricEncryptionAlgorithm(Algorithm): # pylint:disable=bad-option-value,name-too-long - @abstractmethod - def create_encryptor(self, key, iv, auth_data, auth_tag): - raise NotImplementedError() - - @abstractmethod - def create_decryptor(self, key, iv, auth_data, auth_tag): - raise NotImplementedError() - - -class SignatureAlgorithm(Algorithm): - _default_hash_algorithm: "Union[hashes.SHA256, hashes.SHA384, hashes.SHA512, None]" = None - - @property - def default_hash_algorithm(self): - return self._default_hash_algorithm - - @abstractmethod - def create_signature_transform(self, key): - raise NotImplementedError() - - -class HashAlgorithm(Algorithm): - @abstractmethod - def create_digest(self): - raise NotImplementedError() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py deleted file mode 100644 index 76c0368acdcf..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from .aes_cbc import Aes128Cbc, Aes192Cbc, Aes256Cbc, Aes128CbcPad, Aes192CbcPad, Aes256CbcPad -from .aes_cbc_hmac import Aes128CbcHmacSha256, Aes192CbcHmacSha384, Aes256CbcHmacSha512 -from .aes_kw import AesKw128, AesKw192, AesKw256 -from .ecdsa import Ecdsa256, Es256, Es384, Es512 -from .rsa_encryption import Rsa1_5, RsaOaep, RsaOaep256 -from .rsa_signing import Ps256, Ps384, Ps512, Rs256, Rs384, Rs512 - -__all__ = [ - "Aes128Cbc", - "Aes192Cbc", - "Aes256Cbc", - "Aes128CbcPad", - "Aes192CbcPad", - "Aes256CbcPad", - "Aes128CbcHmacSha256", - "Aes192CbcHmacSha384", - "Aes256CbcHmacSha512", - "AesKw128", - "AesKw192", - "AesKw256", - "Ecdsa256", - "Es256", - "Es384", - "Es512", - "Ps256", - "Ps384", - "Ps512", - "Rsa1_5", - "Rs256", - "Rs384", - "Rs512", - "RsaOaep", - "RsaOaep256", -] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py deleted file mode 100644 index 618ffaf370ae..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py +++ /dev/null @@ -1,145 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import padding - -from ..algorithm import SymmetricEncryptionAlgorithm -from ..transform import BlockCryptoTransform - - -# pylint: disable=W0223 - -_CBC_BLOCK_SIZE = 128 - - -class _AesCbcCryptoTransform(BlockCryptoTransform): - def __init__(self, key, iv): - super(_AesCbcCryptoTransform, self).__init__(key) - self._cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - - def transform(self, data): - return self.update(data) + self.finalize() - - def block_size(self): - return _CBC_BLOCK_SIZE - - -class _AesCbcDecryptor(_AesCbcCryptoTransform): - def __init__(self, key, iv, padding_mode): - super(_AesCbcDecryptor, self).__init__(key, iv) - self._ctx = self._cipher.decryptor() - self._padder = padding_mode.unpadder() - - def update(self, data): - decrypted = self._ctx.update(data) + self._ctx.finalize() - return self._padder.update(decrypted) - - def finalize(self): - return self._padder.finalize() - - -class _AesCbcEncryptor(_AesCbcCryptoTransform): - def __init__(self, key, iv, padding_mode): - super(_AesCbcEncryptor, self).__init__(key, iv) - self._ctx = self._cipher.encryptor() - self._padder = padding_mode.padder() - - def update(self, data): - padded = self._padder.update(data) + self._padder.finalize() - return self._ctx.update(padded) - - def finalize(self): - return self._ctx.finalize() - - -class _AesCbc(SymmetricEncryptionAlgorithm): - _key_size = 256 - _block_size = _CBC_BLOCK_SIZE - - def block_size(self): - return self._block_size - - def block_size_in_bytes(self): - return self._block_size >> 3 - - def key_size(self): - return self._key_size - - def key_size_in_bytes(self): - return self._key_size >> 3 - - def create_encryptor(self, key, iv): - key, iv = self._validate_input(key, iv) - - return _AesCbcEncryptor(key, iv, padding.PKCS7(self._block_size)) - - def create_decryptor(self, key, iv): - key, iv = self._validate_input(key, iv) - - return _AesCbcDecryptor(key, iv, padding.PKCS7(self._block_size)) - - def _validate_input(self, key, iv): - if not key: - raise ValueError("A key is required for AES-CBC and AES-CBCPAD encryption and decryption") - if len(key) < self.key_size_in_bytes(): - raise ValueError(f"key must be at least {self.key_size} bits") - - if not iv: - raise ValueError("A 16-byte iv is required for AES-CBC and AES-CBCPAD encryption and decryption") - if not len(iv) == self.block_size_in_bytes(): - raise ValueError(f"iv must be {self.block_size} bits") - - return key[: self.key_size_in_bytes()], iv - - -class _AesCbcPad(_AesCbc): - def create_encryptor(self, key, iv): - key, iv = self._validate_input(key, iv) - - return _AesCbcEncryptor(key, iv, padding.PKCS7(self._block_size)) - - def create_decryptor(self, key, iv): - key, iv = self._validate_input(key, iv) - - return _AesCbcDecryptor(key, iv, padding.PKCS7(self._block_size)) - - -class Aes128Cbc(_AesCbc): - _name = "A128CBC" - _key_size = 128 - - -class Aes128CbcPad(_AesCbcPad): - _name = "A128CBCPAD" - _key_size = 128 - - -class Aes192Cbc(_AesCbc): - _name = "A192CBC" - _key_size = 192 - - -class Aes192CbcPad(_AesCbcPad): - _name = "A192CBCPAD" - _key_size = 192 - - -class Aes256Cbc(_AesCbc): - _name = "A256CBC" - _key_size = 256 - - -class Aes256CbcPad(_AesCbcPad): - _name = "A256CBCPAD" - _key_size = 256 - - -Aes128Cbc.register() -Aes128CbcPad.register() -Aes192Cbc.register() -Aes192CbcPad.register() -Aes256Cbc.register() -Aes256CbcPad.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py deleted file mode 100644 index 4fad959fade9..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py +++ /dev/null @@ -1,149 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from abc import abstractmethod - -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import padding, hashes, hmac - -from ..algorithm import AuthenticatedSymmetricEncryptionAlgorithm -from ..transform import AuthenticatedCryptoTransform -from .._internal import _int_to_fixed_length_bigendian_bytes - - -class _AesCbcHmacCryptoTransform(AuthenticatedCryptoTransform): - def __init__(self, key, iv, auth_data, auth_tag): - super(_AesCbcHmacCryptoTransform, self).__init__() - - self._aes_key = key[: len(key) // 2] - self._hmac_key = key[len(key) // 2 :] - hash_algorithm = {256: hashes.SHA256(), 384: hashes.SHA384(), 512: hashes.SHA512()}[len(key) * 8] - - self._cipher = Cipher(algorithms.AES(self._aes_key), modes.CBC(iv), backend=default_backend()) - self._tag = auth_tag or bytearray() - self._hmac = hmac.HMAC(self._hmac_key, hash_algorithm, backend=default_backend()) - self._auth_data_length = _int_to_fixed_length_bigendian_bytes(len(auth_data) * 8, 8) - - # prime the hash - self._hmac.update(auth_data) - self._hmac.update(iv) - - def tag(self): - return self._tag - - def block_size(self): - # return self._cipher.block_size - raise NotImplementedError() - - @abstractmethod - def update(self, data): - raise NotImplementedError() - - @abstractmethod - def finalize(self): - raise NotImplementedError() - - def transform(self, data): - return self.update(data) + self.finalize() - - -class _AesCbcHmacEncryptor(_AesCbcHmacCryptoTransform): - def __init__(self, key, iv, auth_data, auth_tag): - super(_AesCbcHmacEncryptor, self).__init__(key, iv, auth_data, auth_tag) - self._ctx = self._cipher.encryptor() - self._padder = padding.PKCS7(self.block_size).padder() - self._tag[:] = [] - - def update(self, data): - padded = self._padder.update(data) - cipher_text = self._ctx.update(padded) - self._hmac.update(cipher_text) - return cipher_text - - def finalize(self): - padded = self._padder.finalize() - cipher_text = self._ctx.update(padded) + self._ctx.finalize() - self._hmac.update(cipher_text) - self._hmac.update(self._auth_data_length) - self._tag.extend(self._hmac.finalize()[: len(self._hmac_key)]) - return cipher_text - - def block_size(self): - raise NotImplementedError() - - -class _AesCbcHmacDecryptor(_AesCbcHmacCryptoTransform): - def __init__(self, key, iv, auth_data, auth_tag): - super(_AesCbcHmacDecryptor, self).__init__(key, iv, auth_data, auth_tag) - self._ctx = self._cipher.decryptor() - self._padder = padding.PKCS7(self.block_size).unpadder() - - def update(self, data): - self._hmac.update(data) - padded = self._ctx.update(data) - return self._padder.update(padded) - - def finalize(self): - self._hmac.update(self._auth_data_length) - self._hmac.verify(self.tag) - padded = self._ctx.finalize() - return self._padder.update(padded) + self._padder.finalize() - - # override transform from the base so we can verify the entire hash before we start decrypting - def transform(self, data): - self._hmac.update(data) - self._hmac.update(self._auth_data_length) - self._hmac.verify(self.tag) - padded = self._ctx.update(data) + self._ctx.finalize() - return self._padder.update(padded) + self._padder.finalize() - - def block_size(self): - raise NotImplementedError() - - -class _AesCbcHmac(AuthenticatedSymmetricEncryptionAlgorithm): - _key_size = 256 - - @property - def block_size(self): - return self._key_size // 2 - - @property - def block_size_in_bytes(self): - return self.block_size >> 3 - - @property - def key_size(self): - return self._key_size - - @property - def key_size_in_bytes(self): - return self._key_size >> 3 - - def create_encryptor(self, key, iv, auth_data, auth_tag=None): - return _AesCbcHmacEncryptor(key, iv, auth_data, auth_tag) - - def create_decryptor(self, key, iv, auth_data, auth_tag): - return _AesCbcHmacDecryptor(key, iv, auth_data, auth_tag) - - -class Aes128CbcHmacSha256(_AesCbcHmac): - _key_size = 256 - _name = "A128CBC-HS256" - - -class Aes192CbcHmacSha384(_AesCbcHmac): - _key_size = 384 - _name = "A192CBC-HS384" - - -class Aes256CbcHmacSha512(_AesCbcHmac): - _key_size = 512 - _name = "A256CBC-HS512" - - -Aes128CbcHmacSha256.register() -Aes192CbcHmacSha384.register() -Aes256CbcHmacSha512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py deleted file mode 100644 index 4990de7331bb..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py +++ /dev/null @@ -1,68 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from cryptography.hazmat.primitives.keywrap import aes_key_wrap, aes_key_unwrap -from cryptography.hazmat.backends import default_backend - -from ..algorithm import AsymmetricEncryptionAlgorithm -from ..transform import CryptoTransform -from ..._enums import KeyWrapAlgorithm - - -class _AesKeyWrapTransform(CryptoTransform): - def transform(self, data): - return aes_key_wrap(self._key, data, default_backend()) - - -class _AesKeyUnwrapTransform(CryptoTransform): - def transform(self, data): - return aes_key_unwrap(self._key, data, default_backend()) - - -class _AesKeyWrap(AsymmetricEncryptionAlgorithm): - _key_size = 256 - - @property - def key_size(self): - return self._key_size - - @property - def key_size_in_bytes(self): - return self._key_size >> 3 - - def create_encryptor(self, key): - key = self._validate_input(key) - return _AesKeyWrapTransform(key) - - def create_decryptor(self, key): - key = self._validate_input(key) - return _AesKeyUnwrapTransform(key) - - def _validate_input(self, key): - if not key: - raise ValueError("key") - if len(key) < self.key_size_in_bytes: - raise ValueError(f"key must be at least {self.key_size} bits") - - return key[: self.key_size_in_bytes] - - -class AesKw128(_AesKeyWrap): - _key_size = 128 - _name = "A128KW" - - -class AesKw192(_AesKeyWrap): - _key_size = 192 - _name = "A192KW" - - -class AesKw256(_AesKeyWrap): - _key_size = 256 - _name = KeyWrapAlgorithm.aes_256 - - -AesKw128.register() -AesKw192.register() -AesKw256.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py deleted file mode 100644 index 2c3a6c8bd132..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py +++ /dev/null @@ -1,60 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.asymmetric import utils - -from ..algorithm import SignatureAlgorithm -from ..transform import SignatureTransform -from ..._enums import SignatureAlgorithm as KeyVaultSignatureAlgorithm - - -class _EcdsaSignatureTransform(SignatureTransform): - def __init__(self, key, hash_algorithm): - super(_EcdsaSignatureTransform, self).__init__() - - self._key = key - self._hash_algorithm = hash_algorithm - - def sign(self, digest): - return self._key.sign(digest, ec.ECDSA(utils.Prehashed(self._hash_algorithm))) - - def verify(self, digest, signature): - return self._key.verify(signature, digest, ec.ECDSA(utils.Prehashed(self._hash_algorithm))) - - -class _Ecdsa(SignatureAlgorithm): - def create_signature_transform(self, key): - return _EcdsaSignatureTransform(key, self.default_hash_algorithm) - - -class Ecdsa256(_Ecdsa): - _name = KeyVaultSignatureAlgorithm.es256_k - _default_hash_algorithm = hashes.SHA256() - coordinate_length = 32 - - -class Es256(_Ecdsa): - _name = KeyVaultSignatureAlgorithm.es256 - _default_hash_algorithm = hashes.SHA256() - coordinate_length = 32 - - -class Es384(_Ecdsa): - _name = KeyVaultSignatureAlgorithm.es384 - _default_hash_algorithm = hashes.SHA384() - coordinate_length = 48 - - -class Es512(_Ecdsa): - _name = KeyVaultSignatureAlgorithm.es512 - _default_hash_algorithm = hashes.SHA512() - coordinate_length = 66 - - -Ecdsa256.register() -Es256.register() -Es384.register() -Es512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py deleted file mode 100644 index df83c67365d2..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py +++ /dev/null @@ -1,79 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import padding - -from ..algorithm import AsymmetricEncryptionAlgorithm -from ..transform import CryptoTransform -from ..._enums import EncryptionAlgorithm - - -class _Rsa1_5Encryptor(CryptoTransform): - def transform(self, data): - return self._key.encrypt(data, padding.PKCS1v15()) - - -class _Rsa1_5Decryptor(CryptoTransform): - def transform(self, data): - return self._key.decrypt(data, padding.PKCS1v15()) - - -class Rsa1_5(AsymmetricEncryptionAlgorithm): # pylint:disable=client-incorrect-naming-convention - _name = EncryptionAlgorithm.rsa1_5 - - def create_encryptor(self, key): - return _Rsa1_5Encryptor(key) - - def create_decryptor(self, key): - return _Rsa1_5Decryptor(key) - - -class _RsaOaepDecryptor(CryptoTransform): - def __init__(self, key, hash_cls): - self._hash_cls = hash_cls - super(_RsaOaepDecryptor, self).__init__(key) - - def transform(self, data): - oaep_padding = padding.OAEP( - mgf=padding.MGF1(algorithm=self._hash_cls()), algorithm=self._hash_cls(), label=None - ) - return self._key.decrypt(data, oaep_padding) - - -class _RsaOaepEncryptor(CryptoTransform): - def __init__(self, key, hash_cls): - self._hash_cls = hash_cls - super(_RsaOaepEncryptor, self).__init__(key) - - def transform(self, data): - oaep_padding = padding.OAEP( - mgf=padding.MGF1(algorithm=self._hash_cls()), algorithm=self._hash_cls(), label=None - ) - return self._key.encrypt(data, oaep_padding) - - -class RsaOaep(AsymmetricEncryptionAlgorithm): - _name = EncryptionAlgorithm.rsa_oaep - - def create_encryptor(self, key): - return _RsaOaepEncryptor(key, hashes.SHA1) - - def create_decryptor(self, key): - return _RsaOaepDecryptor(key, hashes.SHA1) - - -class RsaOaep256(AsymmetricEncryptionAlgorithm): - _name = EncryptionAlgorithm.rsa_oaep_256 - - def create_encryptor(self, key): - return _RsaOaepEncryptor(key, hashes.SHA256) - - def create_decryptor(self, key): - return _RsaOaepDecryptor(key, hashes.SHA256) - - -Rsa1_5.register() -RsaOaep.register() -RsaOaep256.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py deleted file mode 100644 index 984befca583a..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py +++ /dev/null @@ -1,75 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import padding, utils - -from ..algorithm import SignatureAlgorithm -from ..transform import SignatureTransform -from ..._enums import SignatureAlgorithm as KeyVaultSignatureAlgorithm - - -class RsaSignatureTransform(SignatureTransform): - def __init__(self, key, padding_function, hash_algorithm): - super(RsaSignatureTransform, self).__init__() - self._key = key - self._padding_function = padding_function - self._hash_algorithm = hash_algorithm - - def sign(self, digest): - return self._key.sign(digest, self._padding_function(digest), utils.Prehashed(self._hash_algorithm)) - - def verify(self, digest, signature): - self._key.verify(signature, digest, self._padding_function(digest), utils.Prehashed(self._hash_algorithm)) - - -class RsaSsaPkcs1v15(SignatureAlgorithm): - def create_signature_transform(self, key): - return RsaSignatureTransform(key, lambda _: padding.PKCS1v15(), self._default_hash_algorithm) - - -class RsaSsaPss(SignatureAlgorithm): - def create_signature_transform(self, key): - return RsaSignatureTransform(key, self._get_padding, self._default_hash_algorithm) - - def _get_padding(self, digest): - return padding.PSS(mgf=padding.MGF1(self._default_hash_algorithm), salt_length=len(digest)) - - -class Ps256(RsaSsaPss): - _name = KeyVaultSignatureAlgorithm.ps256 - _default_hash_algorithm = hashes.SHA256() - - -class Ps384(RsaSsaPss): - _name = KeyVaultSignatureAlgorithm.ps384 - _default_hash_algorithm = hashes.SHA384() - - -class Ps512(RsaSsaPss): - _name = KeyVaultSignatureAlgorithm.ps512 - _default_hash_algorithm = hashes.SHA512() - - -class Rs256(RsaSsaPkcs1v15): - _name = KeyVaultSignatureAlgorithm.rs256 - _default_hash_algorithm = hashes.SHA256() - - -class Rs384(RsaSsaPkcs1v15): - _name = KeyVaultSignatureAlgorithm.rs384 - _default_hash_algorithm = hashes.SHA384() - - -class Rs512(RsaSsaPkcs1v15): - _name = KeyVaultSignatureAlgorithm.rs512 - _default_hash_algorithm = hashes.SHA512() - - -Ps256.register() -Ps384.register() -Ps512.register() -Rs256.register() -Rs384.register() -Rs512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py deleted file mode 100644 index 34e4a0bc3bbc..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py +++ /dev/null @@ -1,53 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Union, Type - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes - -from ..algorithm import HashAlgorithm -from ..transform import DigestTransform - - -class _Sha2DigestTransform(DigestTransform): - def __init__(self, algorithm): - super(_Sha2DigestTransform, self).__init__() - self._digest = hashes.Hash(algorithm=algorithm, backend=default_backend()) - - def update(self, data): - return self._digest.update(data) - - def finalize(self, data): - return self._digest.finalize() - - -class _Sha2HashAlgorithm(HashAlgorithm): - - _algorithm_cls: Union[Type[hashes.SHA256], Type[hashes.SHA384], Type[hashes.SHA512], None] = None - - def create_digest(self): - return _Sha2DigestTransform(self._algorithm_cls()) # pylint:disable=not-callable - - -class Sha256(_Sha2HashAlgorithm): - _algorithm_cls = hashes.SHA256 - _name = "SHA256" - - -class Sha384(_Sha2HashAlgorithm): - _algorithm_cls = hashes.SHA384 - _name = "SHA384" - - -class Sha512(_Sha2HashAlgorithm): - _algorithm_cls = hashes.SHA512 - _name = "SHA512" - - -Sha256.register() - -Sha384.register() - -Sha512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py deleted file mode 100644 index 481a6fd45241..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py +++ /dev/null @@ -1,106 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import uuid - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.ec import ( - EllipticCurvePrivateKey, - EllipticCurvePrivateNumbers, - EllipticCurvePublicNumbers, - SECP256R1, - SECP384R1, - SECP521R1, - SECP256K1, -) - -from ._internal import _bytes_to_int, asn1_der_to_ecdsa, ecdsa_to_asn1_der -from .key import Key -from .algorithms.ecdsa import Es256, Es512, Es384, Ecdsa256 -from ... import KeyCurveName - -_crypto_crv_to_kv_crv = { - "secp256r1": KeyCurveName.p_256, - "secp384r1": KeyCurveName.p_384, - "secp521r1": KeyCurveName.p_521, - "secp256k1": KeyCurveName.p_256_k, -} -_kv_crv_to_crypto_cls = { - KeyCurveName.p_256: SECP256R1, - KeyCurveName.p_256_k: SECP256K1, - KeyCurveName.p_384: SECP384R1, - KeyCurveName.p_521: SECP521R1, - "SECP256K1": SECP256K1, # "SECP256K1" is from Key Vault 2016-10-01 -} -_curve_to_default_algorithm = { - KeyCurveName.p_256: Es256.name(), - KeyCurveName.p_256_k: Ecdsa256.name(), - KeyCurveName.p_384: Es384.name(), - KeyCurveName.p_521: Es512.name(), - "SECP256K1": Ecdsa256.name(), # "SECP256K1" is from Key Vault 2016-10-01 -} - - -class EllipticCurveKey(Key): - _supported_signature_algorithms = frozenset(_curve_to_default_algorithm.values()) - - def __init__(self, x, y, d=None, kid=None, curve=None): - super(EllipticCurveKey, self).__init__() - - self._kid = kid or str(uuid.uuid4()) - self._default_algorithm = _curve_to_default_algorithm[curve] - curve_cls = _kv_crv_to_crypto_cls[curve] - - public_numbers = EllipticCurvePublicNumbers(x, y, curve_cls()) - self._public_key = public_numbers.public_key(default_backend()) - self._private_key = None - if d is not None: - private_numbers = EllipticCurvePrivateNumbers(d, public_numbers) - self._private_key = private_numbers.private_key(default_backend()) - - @classmethod - def from_jwk(cls, jwk): - if jwk.kty not in ("EC", "EC-HSM"): - raise ValueError("The specified key must be of type 'EC' or 'EC-HSM'") - - if not jwk.x or not jwk.y: - raise ValueError("jwk must have values for 'x' and 'y'") - - x = _bytes_to_int(jwk.x) - y = _bytes_to_int(jwk.y) - d = _bytes_to_int(jwk.d) if jwk.d is not None else None - return cls(x, y, d, kid=jwk.kid, curve=jwk.crv) - - def is_private_key(self): - return isinstance(self._private_key, EllipticCurvePrivateKey) - - def decrypt(self, cipher_text, **kwargs): - raise NotImplementedError("Local decryption isn't supported with elliptic curve keys") - - def encrypt(self, plain_text, **kwargs): - raise NotImplementedError("Local encryption isn't supported with elliptic curve keys") - - def wrap_key(self, key, **kwargs): - raise NotImplementedError("Local key wrapping isn't supported with elliptic curve keys") - - def unwrap_key(self, encrypted_key, **kwargs): - raise NotImplementedError("Local key unwrapping isn't supported with elliptic curve keys") - - def sign(self, digest, **kwargs): - algorithm = self._get_algorithm("sign", **kwargs) - signer = algorithm.create_signature_transform(self._private_key) - signature = signer.sign(digest) - ecdsa_signature = asn1_der_to_ecdsa(signature, algorithm) - return ecdsa_signature - - def verify(self, digest, signature, **kwargs): - algorithm = self._get_algorithm("verify", **kwargs) - signer = algorithm.create_signature_transform(self._public_key) - asn1_signature = ecdsa_to_asn1_der(signature) - try: - signer.verify(digest, asn1_signature) - return True - except InvalidSignature: - return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py deleted file mode 100644 index 82298feb1654..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py +++ /dev/null @@ -1,94 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -from abc import ABCMeta, abstractmethod -from typing import Any, FrozenSet - -from .algorithm import Algorithm - - -class Key(object, metaclass=ABCMeta): - _supported_encryption_algorithms: FrozenSet[Any] = frozenset([]) - _supported_key_wrap_algorithms: FrozenSet[Any] = frozenset([]) - _supported_signature_algorithms: FrozenSet[Any] = frozenset([]) - - def __init__(self): - self._kid = None - - @property - def default_encryption_algorithm(self): - return None - - @property - def default_key_wrap_algorithm(self): - return None - - @property - def default_signature_algorithm(self): - return None - - @property - def supported_encryption_algorithms(self): - return self._supported_encryption_algorithms - - @property - def supported_key_wrap_algorithms(self): - return self._supported_key_wrap_algorithms - - @property - def supported_signature_algorithms(self): - return self._supported_signature_algorithms - - @property - def kid(self): - return self._kid - - @abstractmethod - def is_private_key(self): - pass - - @abstractmethod - def decrypt(self, cipher_text, **kwargs): - raise NotImplementedError() - - @abstractmethod - def encrypt(self, plain_text, **kwargs): - raise NotImplementedError() - - @abstractmethod - def wrap_key(self, key, **kwargs): - raise NotImplementedError() - - @abstractmethod - def unwrap_key(self, encrypted_key, **kwargs): - raise NotImplementedError() - - @abstractmethod - def sign(self, digest, **kwargs): - raise NotImplementedError() - - @abstractmethod - def verify(self, digest, signature, **kwargs): - raise NotImplementedError() - - def _get_algorithm(self, op, **kwargs): - default_algorithm, supported_algorithms = { - "encrypt": (self.default_encryption_algorithm, self.supported_encryption_algorithms), - "decrypt": (self.default_encryption_algorithm, self.supported_encryption_algorithms), - "wrapKey": (self.default_key_wrap_algorithm, self.supported_key_wrap_algorithms), - "unwrapKey": (self.default_key_wrap_algorithm, self.supported_key_wrap_algorithms), - "sign": (self.default_signature_algorithm, self.supported_signature_algorithms), - "verify": (self.default_signature_algorithm, self.supported_signature_algorithms), - }[op] - - algorithm = kwargs.get("algorithm", default_algorithm) - - if not isinstance(algorithm, Algorithm): - algorithm = Algorithm.resolve(algorithm) - - if not algorithm or not supported_algorithms or algorithm.name() not in supported_algorithms: - raise ValueError(f"unsupported algorithm '{algorithm}'") - - return algorithm diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py deleted file mode 100644 index 0d3f21df4257..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py +++ /dev/null @@ -1,228 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import uuid - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.rsa import ( - RSAPrivateKey, - RSAPrivateNumbers, - RSAPublicNumbers, - generate_private_key, - rsa_crt_dmp1, - rsa_crt_dmq1, - rsa_crt_iqmp, -) - -from ._internal import _bytes_to_int, _int_to_bytes -from .key import Key -from .algorithms import Ps256, Ps384, Ps512, Rsa1_5, RsaOaep, RsaOaep256, Rs256, Rs384, Rs512 -from ... import JsonWebKey, KeyOperation - - -class RsaKey(Key): # pylint:disable=too-many-public-methods - PUBLIC_KEY_DEFAULT_OPS = [KeyOperation.encrypt, KeyOperation.wrap_key, KeyOperation.verify] - PRIVATE_KEY_DEFAULT_OPS = PUBLIC_KEY_DEFAULT_OPS + [ - KeyOperation.decrypt, - KeyOperation.unwrap_key, - KeyOperation.sign, - ] - - _supported_encryption_algorithms = frozenset((Rsa1_5.name(), RsaOaep.name(), RsaOaep256.name())) - _supported_key_wrap_algorithms = frozenset((Rsa1_5.name(), RsaOaep.name(), RsaOaep256.name())) - _supported_signature_algorithms = frozenset( - ( - Ps256.name(), - Ps384.name(), - Ps512.name(), - Rs256.name(), - Rs384.name(), - Rs512.name(), - ) - ) - - def __init__(self, kid=None): - super(RsaKey, self).__init__() - self._kid = kid - self.kty = None - self.key_ops = None - self._rsa_impl = None - - @property - def n(self): - return _int_to_bytes(self._public_key_material().n) - - @property - def e(self): - return _int_to_bytes(self._public_key_material().e) - - @property - def p(self): - return _int_to_bytes(self._private_key_material().p) if self.is_private_key() else None - - @property - def q(self): - return _int_to_bytes(self._private_key_material().q) if self.is_private_key() else None - - @property - def b(self): - return _int_to_bytes(self._private_key_material().b) if self.is_private_key() else None - - @property - def d(self): - return _int_to_bytes(self._private_key_material().d) if self.is_private_key() else None - - @property - def dq(self): - return _int_to_bytes(self._private_key_material().dmq1) if self.is_private_key() else None - - @property - def dp(self): - return _int_to_bytes(self._private_key_material().dmp1) if self.is_private_key() else None - - @property - def qi(self): - return _int_to_bytes(self._private_key_material().iqmp) if self.is_private_key() else None - - @property - def private_key(self): - return self._rsa_impl if self.is_private_key() else None - - @property - def public_key(self): - return self._rsa_impl.public_key() if self.is_private_key() else self._rsa_impl - - @staticmethod - def generate(kid=None, kty="RSA", size=2048, e=65537): - key = RsaKey() - key.kid = kid or str(uuid.uuid4()) - key.kty = kty - key.key_ops = RsaKey.PRIVATE_KEY_DEFAULT_OPS - # pylint:disable=protected-access - key._rsa_impl = generate_private_key(public_exponent=e, key_size=size, backend=default_backend()) - return key - - @classmethod - def from_jwk(cls, jwk): - if jwk.kty not in ("RSA", "RSA-HSM"): - raise ValueError('The specified jwk must have a key type of "RSA" or "RSA-HSM"') - - if not jwk.n or not jwk.e: - raise ValueError("Invalid RSA jwk, both n and e must be have values") - - rsa_key = cls(kid=jwk.kid) - rsa_key.kty = jwk.kty - rsa_key.key_ops = jwk.key_ops - - pub = RSAPublicNumbers(n=_bytes_to_int(jwk.n), e=_bytes_to_int(jwk.e)) - - # if the private key values are specified construct a private key - # only the secret primes and private exponent are needed as other fields can be calculated - if jwk.p and jwk.q and jwk.d: - # convert the values of p, q, and d from bytes to int - p = _bytes_to_int(jwk.p) - q = _bytes_to_int(jwk.q) - d = _bytes_to_int(jwk.d) - - # convert or compute the remaining private key numbers - dmp1 = _bytes_to_int(jwk.dp) if jwk.dp else rsa_crt_dmp1(private_exponent=d, p=p) - dmq1 = _bytes_to_int(jwk.dq) if jwk.dq else rsa_crt_dmq1(private_exponent=d, q=q) - iqmp = _bytes_to_int(jwk.qi) if jwk.qi else rsa_crt_iqmp(p=p, q=q) - - # create the private key from the jwk key values - priv = RSAPrivateNumbers(p=p, q=q, d=d, dmp1=dmp1, dmq1=dmq1, iqmp=iqmp, public_numbers=pub) - key_impl = priv.private_key(default_backend()) - - # if the necessary private key values are not specified create the public key - else: - key_impl = pub.public_key(default_backend()) - - rsa_key._rsa_impl = key_impl - - return rsa_key - - def to_jwk(self, include_private=False): - jwk = JsonWebKey( - kid=self.kid, - kty=self.kty, - key_ops=self.key_ops if include_private else RsaKey.PUBLIC_KEY_DEFAULT_OPS, - n=self.n, - e=self.e, - ) - - if include_private: - jwk.q = self.q - jwk.p = self.p - jwk.d = self.d - jwk.dq = self.dq - jwk.dp = self.dp - jwk.qi = self.qi - - return jwk - - @property - def default_encryption_algorithm(self): - return RsaOaep.name() - - @property - def default_key_wrap_algorithm(self): - return RsaOaep.name() - - @property - def default_signature_algorithm(self): - return Rs256.name() - - def encrypt(self, plain_text, **kwargs): - algorithm = self._get_algorithm("encrypt", **kwargs) - encryptor = algorithm.create_encryptor(self.public_key) - return encryptor.transform(plain_text) - - def decrypt(self, cipher_text, **kwargs): - if not self.is_private_key(): - raise NotImplementedError("The current RsaKey does not support decrypt") - - algorithm = self._get_algorithm("decrypt", **kwargs) - decryptor = algorithm.create_decryptor(self.private_key) - return decryptor.transform(cipher_text) - - def sign(self, digest, **kwargs): - if not self.is_private_key(): - raise NotImplementedError("The current RsaKey does not support sign") - - algorithm = self._get_algorithm("sign", **kwargs) - signer = algorithm.create_signature_transform(self.private_key) - return signer.sign(digest) - - def verify(self, digest, signature, **kwargs): - algorithm = self._get_algorithm("verify", **kwargs) - signer = algorithm.create_signature_transform(self.public_key) - try: - # cryptography's verify methods return None, and raise when verification fails - signer.verify(digest, signature) - return True - except InvalidSignature: - return False - - def wrap_key(self, key, **kwargs): - algorithm = self._get_algorithm("wrapKey", **kwargs) - encryptor = algorithm.create_encryptor(self.public_key) - return encryptor.transform(key) - - def unwrap_key(self, encrypted_key, **kwargs): - if not self.is_private_key(): - raise NotImplementedError("The current RsaKey does not support unwrap") - - algorithm = self._get_algorithm("unwrapKey", **kwargs) - decryptor = algorithm.create_decryptor(self.private_key) - return decryptor.transform(encrypted_key) - - def is_private_key(self): - return isinstance(self._rsa_impl, RSAPrivateKey) - - def _public_key_material(self): - return self.public_key.public_numbers() - - def _private_key_material(self): - return self.private_key.private_numbers() if self.private_key else None diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py deleted file mode 100644 index 1682b1ac4bd0..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py +++ /dev/null @@ -1,125 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import uuid -import os - -from azure.core.exceptions import AzureError -from .key import Key -from .algorithms.aes_cbc import Aes256CbcPad, Aes192CbcPad, Aes128CbcPad -from .algorithms.aes_cbc_hmac import Aes256CbcHmacSha512, Aes192CbcHmacSha384 -from .algorithms.aes_kw import AesKw256, AesKw192, AesKw128 - -key_size_128 = 128 >> 3 -key_size_192 = 192 >> 3 -key_size_256 = 256 >> 3 -key_size_384 = 384 >> 3 -key_size_512 = 512 >> 3 - -_default_key_size = key_size_256 - -_supported_key_sizes = [key_size_128, key_size_192, key_size_256, key_size_384, key_size_512] - -_default_enc_alg_by_size = { - key_size_128: Aes128CbcPad.name(), - key_size_192: Aes192CbcPad.name(), - key_size_256: Aes256CbcPad.name(), - key_size_384: Aes192CbcHmacSha384.name(), - key_size_512: Aes256CbcHmacSha512.name(), -} - -_default_kw_alg_by_size = { - key_size_128: AesKw128.name(), - key_size_192: AesKw192.name(), - key_size_256: AesKw256.name(), - key_size_384: AesKw256.name(), - key_size_512: AesKw256.name(), -} - - -def raise_if_incorrect_key_size(algorithm, key_size): - if algorithm._key_size >> 3 != key_size: # pylint:disable=protected-access - raise AzureError("Invalid AES encryption algorithm for key size. The algorithm must match the size of the key.") - - -class SymmetricKey(Key): - def __init__(self, kid=None, key_bytes=None, key_size=None): - super(SymmetricKey, self).__init__() - - self._kid = kid or str(uuid.uuid4()) - - if not key_bytes: - key_size = key_size or _default_key_size - - if key_size not in _supported_key_sizes: - raise ValueError("The key size must be 128, 192, 256, 384 or 512 bits of data") - - key_bytes = os.urandom(key_size) - - if len(key_bytes) not in _supported_key_sizes: - raise ValueError("The key size must be 128, 192, 256, 384 or 512 bits of data") - - self._key = key_bytes - - supported_encryption_algorithms = [] - supported_key_wrap_algorithms = [] - key_size = len(self._key) - if key_size >= key_size_128: - supported_encryption_algorithms.append(Aes128CbcPad.name()) - supported_key_wrap_algorithms.append(AesKw128.name()) - if key_size >= key_size_192: - supported_encryption_algorithms.append(Aes192CbcPad.name()) - supported_key_wrap_algorithms.append(AesKw192.name()) - if key_size >= key_size_256: - supported_encryption_algorithms.append(Aes256CbcPad.name()) - supported_key_wrap_algorithms.append(AesKw256.name()) - self._supported_encryption_algorithms = frozenset(supported_encryption_algorithms) - self._supported_key_wrap_algorithms = frozenset(supported_key_wrap_algorithms) - - def is_private_key(self): - return True - - @classmethod - def from_jwk(cls, jwk): - return cls(kid=jwk.kid, key_bytes=jwk.k) - - @property - def kid(self): - return self._kid - - @property - def default_encryption_algorithm(self): - return _default_enc_alg_by_size[len(self._key)] - - @property - def default_key_wrap_algorithm(self): - return _default_kw_alg_by_size[len(self._key)] - - def encrypt(self, plain_text, iv, **kwargs): # pylint:disable=arguments-differ - algorithm = self._get_algorithm("encrypt", **kwargs) - raise_if_incorrect_key_size(algorithm, len(self._key)) - encryptor = algorithm.create_encryptor(key=self._key, iv=iv) - return encryptor.transform(plain_text) - - def decrypt(self, cipher_text, iv, **kwargs): # pylint:disable=arguments-differ - algorithm = self._get_algorithm("decrypt", **kwargs) - raise_if_incorrect_key_size(algorithm, len(self._key)) - decryptor = algorithm.create_decryptor(key=self._key, iv=iv) - return decryptor.transform(cipher_text) - - def wrap_key(self, key, **kwargs): - algorithm = self._get_algorithm("wrapKey", **kwargs) - encryptor = algorithm.create_encryptor(key=self._key) - return encryptor.transform(key) - - def unwrap_key(self, encrypted_key, **kwargs): - algorithm = self._get_algorithm("unwrapKey", **kwargs) - decryptor = algorithm.create_decryptor(key=self._key) - return decryptor.transform(encrypted_key) - - def sign(self, digest, **kwargs): - raise NotImplementedError("Local signing isn't supported with symmetric keys") - - def verify(self, digest, signature, **kwargs): - raise NotImplementedError("Local signature verification isn't supported with symmetric keys") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py deleted file mode 100644 index 3a24f7ab1a7f..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py +++ /dev/null @@ -1,61 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -from abc import ABCMeta, abstractmethod - - -class CryptoTransform(object, metaclass=ABCMeta): - def __init__(self, key): - self._key = key - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self._key = None - - @abstractmethod - def transform(self, data): - raise NotImplementedError() - - -class BlockCryptoTransform(CryptoTransform): - @abstractmethod - def block_size(self): - raise NotImplementedError() - - @abstractmethod - def update(self, data): - raise NotImplementedError() - - @abstractmethod - def finalize(self): - raise NotImplementedError() - - -class AuthenticatedCryptoTransform(object, metaclass=ABCMeta): - @abstractmethod - def tag(self): - raise NotImplementedError() - - -class SignatureTransform(object, metaclass=ABCMeta): - @abstractmethod - def sign(self, digest): - raise NotImplementedError() - - @abstractmethod - def verify(self, digest, signature): - raise NotImplementedError() - - -class DigestTransform(object, metaclass=ABCMeta): - @abstractmethod - def update(self, data): - raise NotImplementedError() - - @abstractmethod - def finalize(self, data): - raise NotImplementedError() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py deleted file mode 100644 index 4e879040759b..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py +++ /dev/null @@ -1,16 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from datetime import datetime, timezone -from typing import Optional - - -def raise_if_time_invalid(not_before: Optional[datetime], expires_on: Optional[datetime]) -> None: - now = datetime.now(timezone.utc) - if (not_before and expires_on) and not not_before <= now <= expires_on: - raise ValueError(f"This client's key is useable only between {not_before} and {expires_on} (UTC)") - if not_before and not_before > now: - raise ValueError(f"This client's key is not useable until {not_before} (UTC)") - if expires_on and expires_on <= now: - raise ValueError(f"This client's key expired at {expires_on} (UTC)") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py deleted file mode 100644 index d65d152f6240..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ /dev/null @@ -1,621 +0,0 @@ -# pylint: disable=line-too-long,useless-suppression -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from __future__ import annotations -from typing import Any, cast, Optional, NoReturn, Union, TYPE_CHECKING - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS, MGF1 -from cryptography.hazmat.primitives.asymmetric.rsa import ( - rsa_crt_dmp1, - rsa_crt_dmq1, - rsa_crt_iqmp, - rsa_recover_prime_factors, - RSAPrivateKey, - RSAPrivateNumbers, - RSAPublicKey, - RSAPublicNumbers, -) -from cryptography.hazmat.primitives.asymmetric.utils import Prehashed -from cryptography.hazmat.primitives.hashes import Hash, HashAlgorithm, SHA1, SHA256, SHA384, SHA512 -from cryptography.hazmat.primitives.serialization import ( - Encoding, - KeySerializationEncryption, - PrivateFormat, - PublicFormat, -) - -from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm -from .._models import JsonWebKey - -if TYPE_CHECKING: - # Import client only during TYPE_CHECKING to avoid circular dependency - from ._client import CryptographyClient - - -SIGN_ALGORITHM_MAP = { - SHA256: SignatureAlgorithm.rs256, - SHA384: SignatureAlgorithm.rs384, - SHA512: SignatureAlgorithm.rs512, -} -OAEP_MAP = {SHA1: EncryptionAlgorithm.rsa_oaep, SHA256: EncryptionAlgorithm.rsa_oaep_256} -PSS_MAP = { - SignatureAlgorithm.rs256: SignatureAlgorithm.ps256, - SignatureAlgorithm.rs384: SignatureAlgorithm.ps384, - SignatureAlgorithm.rs512: SignatureAlgorithm.ps512, -} - - -def get_encryption_algorithm(padding: AsymmetricPadding) -> EncryptionAlgorithm: - """Maps an `AsymmetricPadding` to an encryption algorithm. - - :param padding: The padding to use. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - - :returns: The corresponding Key Vault encryption algorithm. - :rtype: EncryptionAlgorithm - """ - if isinstance(padding, OAEP): - # Public algorithm property was only added in https://github.com/pyca/cryptography/pull/9582 - # _algorithm property has been available in every version of the OAEP class, so we use it as a backup - try: - algorithm = padding.algorithm # type: ignore[attr-defined] - except AttributeError: - algorithm = padding._algorithm # pylint:disable=protected-access - mapped_algorithm = OAEP_MAP.get(type(algorithm)) - if mapped_algorithm is None: - raise ValueError(f"Unsupported algorithm: {algorithm.name}") - - # Public mgf property was added at the same time as algorithm - try: - mgf = padding.mgf # type: ignore[attr-defined] - except AttributeError: - mgf = padding._mgf # pylint:disable=protected-access - if not isinstance(mgf, MGF1): - raise ValueError(f"Unsupported MGF: {mgf}") - - elif isinstance(padding, PKCS1v15): - mapped_algorithm = EncryptionAlgorithm.rsa1_5 - else: - raise ValueError(f"Unsupported padding: {padding.name}") - - return mapped_algorithm - - -def get_signature_algorithm(padding: AsymmetricPadding, algorithm: HashAlgorithm) -> SignatureAlgorithm: - """Maps an `AsymmetricPadding` and `HashAlgorithm` to a signature algorithm. - - :param padding: The padding to use. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - :param algorithm: The algorithm to use. - :type algorithm: ~cryptography.hazmat.primitives.hashes.HashAlgorithm - - :returns: The corresponding Key Vault signature algorithm. - :rtype: SignatureAlgorithm - """ - mapped_algorithm = SIGN_ALGORITHM_MAP.get(type(algorithm)) - if mapped_algorithm is None: - raise ValueError(f"Unsupported algorithm: {algorithm.name}") - - # If PSS padding is requested, use the PSS equivalent algorithm - if isinstance(padding, PSS): - mapped_algorithm = PSS_MAP.get(mapped_algorithm) - - # Public mgf property was only added in https://github.com/pyca/cryptography/pull/9582 - # _mgf property has been available in every version of the PSS class, so we use it as a backup - try: - mgf = padding.mgf # type: ignore[attr-defined] - except AttributeError: - mgf = padding._mgf # pylint:disable=protected-access - if not isinstance(mgf, MGF1): - raise ValueError(f"Unsupported MGF: {mgf}") - - # The only other padding accepted is PKCS1v15 - elif not isinstance(padding, PKCS1v15): - raise ValueError(f"Unsupported padding: {padding.name}") - - return cast(SignatureAlgorithm, mapped_algorithm) - - -class KeyVaultRSAPublicKey(RSAPublicKey): - """An `RSAPublicKey` implementation based on a key managed by Key Vault. - - This class should not be instantiated directly. Instead, use the - :func:`~azure.keyvault.keys.crypto.CryptographyClient.create_rsa_public_key` method to create a key based on the - client's key. Only synchronous clients and operations are supported at this time. - """ - - def __init__(self, client: "CryptographyClient", key_material: Optional[JsonWebKey] = None) -> None: - self._client: "CryptographyClient" = client - self._key: Optional[JsonWebKey] = key_material - - def encrypt(self, plaintext: bytes, padding: AsymmetricPadding) -> bytes: - """Encrypts the given plaintext. - - :param bytes plaintext: Plaintext to encrypt. - :param padding: The padding to use. Supported paddings are `OAEP` and `PKCS1v15`. For `OAEP` padding, supported - hash algorithms are `SHA1` and `SHA256`. The only supported mask generation function is `MGF1`. See - https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - - :returns: The encrypted ciphertext, as bytes. - :rtype: bytes - """ - mapped_algorithm = get_encryption_algorithm(padding) - result = self._client.encrypt(mapped_algorithm, plaintext) - return result.ciphertext - - @property - def key_size(self) -> int: - """The bit length of the public modulus. - - :returns: The key's size. - :rtype: int - - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(encrypt, verify) can be performed." - ) - - public_key = self.public_numbers().public_key() - return public_key.key_size - - def public_numbers(self) -> RSAPublicNumbers: - """Returns an `RSAPublicNumbers` representing the key's public numbers. - - :returns: The public numbers of the key. - :rtype: RSAPublicNumbers - - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(encrypt, verify) can be performed." - ) - - e = int.from_bytes(self._key.e, "big") # type: ignore[attr-defined] - n = int.from_bytes(self._key.n, "big") # type: ignore[attr-defined] - return RSAPublicNumbers(e, n) - - def public_bytes(self, encoding: Encoding, format: PublicFormat) -> bytes: - """Allows serialization of the key to bytes. - - This function uses the `cryptography` library's implementation. - Encoding (`PEM` or `DER`) and format (`SubjectPublicKeyInfo` or `PKCS1`) are chosen to define the exact - serialization. - - :param encoding: A value from the `Encoding` enum. - :type encoding: ~cryptography.hazmat.primitives.serialization.Encoding - :param format: A value from the `PublicFormat` enum. - :type format: ~cryptography.hazmat.primitives.serialization.PublicFormat - - :returns: The serialized key. - :rtype: bytes - - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(encrypt, verify) can be performed." - ) - - public_key = self.public_numbers().public_key() - return public_key.public_bytes(encoding=encoding, format=format) - - def verify( - self, - signature: bytes, - data: bytes, - padding: AsymmetricPadding, - algorithm: Union[Prehashed, HashAlgorithm], - ) -> None: - """Verifies the signature of the data. - - :param bytes signature: The signature to sign, as bytes. - :param bytes data: The message string that was signed., as bytes. - :param padding: The padding to use. Supported paddings are `PKCS1v15` and `PSS`. For `PSS`, the only supported - mask generation function is `MGF1`. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details - for details. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, - `SHA384`, and `SHA512`. - :type algorithm: ~cryptography.hazmat.primitives.asymmetric.utils.Prehashed or - cryptography.hazmat.primitives.hashes.HashAlgorithm - - :raises InvalidSignature: If the signature does not validate. - """ - if isinstance(algorithm, Prehashed): - raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") - mapped_algorithm = get_signature_algorithm(padding, algorithm) - digest = Hash(algorithm) - digest.update(data) - result = self._client.verify(mapped_algorithm, digest.finalize(), signature) - if not result.is_valid: - raise InvalidSignature(f"The provided signature '{signature!r}' is invalid.") - - def recover_data_from_signature( # type: ignore[override] # Parameter subset - self, - signature: bytes, - padding: AsymmetricPadding, - algorithm: Optional[HashAlgorithm], - ) -> bytes: - # pylint: disable=line-too-long - """Recovers the signed data from the signature. Only supported with `cryptography` version 3.3 and above. - - This function uses the `cryptography` library's implementation. - The data typically contains the digest of the original message string. The `padding` and `algorithm` parameters - must match the ones used when the signature was created for the recovery to succeed. - The `algorithm` parameter can also be set to None to recover all the data present in the signature, without - regard to its format or the hash algorithm used for its creation. - - For `PKCS1v15` padding, this method returns the data after removing the padding layer. For standard signatures - the data contains the full `DigestInfo` structure. For non-standard signatures, any data can be returned, - including zero-length data. - - Normally you should use the `verify()` function to validate the signature. But for some non-standard signature - formats you may need to explicitly recover and validate the signed data. The following are some examples: - - * Some old Thawte and Verisign timestamp certificates without `DigestInfo`. - * Signed MD5/SHA1 hashes in TLS 1.1 or earlier (`RFC 4346 `_, section 4.7). - * IKE version 1 signatures without `DigestInfo` (`RFC 2409 `_, section 5.1). - - :param bytes signature: The signature. - :param padding: An instance of `AsymmetricPadding`. Recovery is only supported with some of the padding types. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - :param algorithm: An instance of `HashAlgorithm`. Can be None to return all the data present in the signature. - :type algorithm: ~cryptography.hazmat.primitives.hashes.HashAlgorithm - - :returns: The signed data. - :rtype: bytes - :raises NotImplementedError: if the local version of `cryptography` doesn't support this method. - :raises ~cryptography.exceptions.InvalidSignature: if the signature is invalid. - :raises ~cryptography.exceptions.UnsupportedAlgorithm: if the signature data recovery is not supported with - the provided `padding` type. - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(encrypt, verify) can be performed." - ) - - public_key = self.public_numbers().public_key() - try: - return public_key.recover_data_from_signature(signature=signature, padding=padding, algorithm=algorithm) - except AttributeError as exc: - raise NotImplementedError( - "This method is only available on `cryptography`>=3.3. Update your package version to use this method." - ) from exc - - def __eq__(self, other: object) -> bool: - """Checks equality. - - :param object other: Another object to compare with this instance. Currently, only comparisons with - `KeyVaultRSAPrivateKey` or `JsonWebKey` instances are supported. - - :returns: True if the objects are equal; False if the objects are unequal or if key material can't be obtained - from Key Vault for comparison. - :rtype: bool - """ - if self._key is None: - return False - - if isinstance(other, KeyVaultRSAPublicKey): - return all(getattr(self._key, field) == getattr(other._key, field) for field in self._key._FIELDS) - if isinstance(other, JsonWebKey): - return all(getattr(self._key, field) == getattr(other, field) for field in self._key._FIELDS) - return False - - def __copy__(self) -> KeyVaultRSAPublicKey: - """Returns this instance since it is treated as immutable. - - :returns: This instance. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey - """ - return self - - def __deepcopy__(self, memo: dict) -> KeyVaultRSAPublicKey: - """Returns this instance since it is treated as immutable. - - :param dict memo: The memo dictionary used by deepcopy. - :returns: This instance. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey - """ - return self - - def verifier( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - self, signature: bytes, padding: AsymmetricPadding, algorithm: HashAlgorithm - ) -> NoReturn: - """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" - raise NotImplementedError() - - -class KeyVaultRSAPrivateKey(RSAPrivateKey): - """An `RSAPrivateKey` implementation based on a key managed by Key Vault. - - This class should not be instantiated directly. Instead, use the - :func:`~azure.keyvault.keys.crypto.CryptographyClient.create_rsa_private_key` method to create a key based on the - client's key. Only synchronous clients and operations are supported at this time. - """ - - def __init__(self, client: "CryptographyClient", key_material: Optional[JsonWebKey]) -> None: - self._client: "CryptographyClient" = client - self._key: Optional[JsonWebKey] = key_material - - def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: - """Decrypts the provided ciphertext. - - :param bytes ciphertext: Encrypted bytes to decrypt. - :param padding: The padding to use. Supported paddings are `OAEP` and `PKCS1v15`. For `OAEP` padding, supported - hash algorithms are `SHA1` and `SHA256`. The only supported mask generation function is `MGF1`. See - https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - - :returns: The decrypted plaintext, as bytes. - :rtype: bytes - """ - mapped_algorithm = get_encryption_algorithm(padding) - result = self._client.decrypt(mapped_algorithm, ciphertext) - return result.plaintext - - @property - def key_size(self) -> int: - """The bit length of the public modulus. - - :returns: The key's size. - :rtype: int - - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(decrypt, sign) can be performed." - ) - - # Key size only requires public modulus, which we can always get - # Relying on private numbers instead would cause issues for keys stored in KV (which doesn't return private key) - return self.public_key().key_size - - def public_key(self) -> KeyVaultRSAPublicKey: - """The `RSAPublicKey` associated with this private key, as a `KeyVaultRSAPublicKey`. - - The public key implementation will use the same underlying cryptography client as this private key. - - :returns: The `KeyVaultRSAPublicKey` associated with the key. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey - """ - return KeyVaultRSAPublicKey(self._client, self._key) - - def sign( # type: ignore[override] # Parameter subset - self, - data: bytes, - padding: AsymmetricPadding, - algorithm: Union[Prehashed, HashAlgorithm], - ) -> bytes: - """Signs the data. - - :param bytes data: The data to sign, as bytes. - :param padding: The padding to use. Supported paddings are `PKCS1v15` and `PSS`. For `PSS`, the only supported - mask generation function is `MGF1`. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details - for details. - :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding - :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, - `SHA384`, and `SHA512`. - :type algorithm: ~cryptography.hazmat.primitives.asymmetric.utils.Prehashed or - cryptography.hazmat.primitives.hashes.HashAlgorithm - - :returns: The signature, as bytes. - :rtype: bytes - """ - if isinstance(algorithm, Prehashed): - raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") - mapped_algorithm = get_signature_algorithm(padding, algorithm) - digest = Hash(algorithm) - digest.update(data) - result = self._client.sign(mapped_algorithm, digest.finalize()) - return result.signature - - def private_numbers(self) -> RSAPrivateNumbers: - """Returns an `RSAPrivateNumbers` representing the key's private numbers. - - :returns: The private numbers of the key. - :rtype: ~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateNumbers - - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(decrypt, sign) can be performed." - ) - - # Fetch public numbers from JWK - e = int.from_bytes(self._key.e, "big") # type: ignore[attr-defined] - n = int.from_bytes(self._key.n, "big") # type: ignore[attr-defined] - public_numbers = RSAPublicNumbers(e, n) - - # Fetch private numbers from JWK - p = int.from_bytes(self._key.p, "big") if self._key.p else None # type: ignore[attr-defined] - q = int.from_bytes(self._key.q, "big") if self._key.q else None # type: ignore[attr-defined] - d = int.from_bytes(self._key.d, "big") if self._key.d else None # type: ignore[attr-defined] - dmp1 = int.from_bytes(self._key.dp, "big") if self._key.dp else None # type: ignore[attr-defined] - dmq1 = int.from_bytes(self._key.dq, "big") if self._key.dq else None # type: ignore[attr-defined] - iqmp = int.from_bytes(self._key.qi, "big") if self._key.qi else None # type: ignore[attr-defined] - - # Calculate any missing attributes - if d is None: - raise ValueError("An 'RSAPrivateNumbers' couldn't be created with the available key material.") - if p is None or q is None: - p, q = rsa_recover_prime_factors(n, e, d) - if dmp1 is None: - dmp1 = rsa_crt_dmp1(d, p) - if dmq1 is None: - dmq1 = rsa_crt_dmq1(d, q) - if iqmp is None: - iqmp = rsa_crt_iqmp(p, q) - - return RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, public_numbers) - - def private_bytes( - self, encoding: Encoding, format: PrivateFormat, encryption_algorithm: KeySerializationEncryption - ) -> bytes: - """Allows serialization of the key to bytes. - - This function uses the `cryptography` library's implementation. - Encoding (`PEM` or `DER`) and format (`TraditionalOpenSSL`, `OpenSSH`, or `PKCS8`) and encryption algorithm - (such as `BestAvailableEncryption` or `NoEncryption`) are chosen to define the exact serialization. - - :param encoding: A value from the `Encoding` enum. - :type encoding: ~cryptography.hazmat.primitives.serialization.Encoding - :param format: A value from the `PrivateFormat` enum. - :type format: ~cryptography.hazmat.primitives.serialization.PrivateFormat - :param encryption_algorithm: An instance of an object conforming to the `KeySerializationEncryption` interface. - :type encryption_algorithm: ~cryptography.hazmat.primitives.serialization.KeySerializationEncryption - - :returns: The serialized key. - :rtype: bytes - - :raises ValueError: if the client is unable to obtain the key material from Key Vault. - """ - if self._key is None: - raise ValueError( - "Key material could not be obtained from Key Vault. Only remote cryptographic operations " - "(decrypt, sign) can be performed." - ) - - try: - private_numbers = self.private_numbers() - except ValueError as exc: - raise ValueError("Insufficient key material to serialize the private key.") from exc - private_key = private_numbers.private_key() - return private_key.private_bytes(encoding=encoding, format=format, encryption_algorithm=encryption_algorithm) - - def signer( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - self, padding: AsymmetricPadding, algorithm: HashAlgorithm - ) -> NoReturn: - """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" - raise NotImplementedError() - - def __copy__(self) -> KeyVaultRSAPrivateKey: - """Returns this instance since it is treated as immutable. - - :returns: This instance. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey - """ - return self - - def __deepcopy__(self, memo: dict) -> KeyVaultRSAPrivateKey: - """Returns this instance since it is treated as immutable. - - :param dict memo: The memo dictionary used by deepcopy. - :returns: This instance. - :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey - """ - return self - - -class DecryptResult: - """The result of a decrypt operation. - - :param str key_id: The encryption key's Key Vault identifier - :param algorithm: The encryption algorithm used - :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm - :param bytes plaintext: The decrypted bytes - """ - - def __init__(self, key_id: Optional[str], algorithm: EncryptionAlgorithm, plaintext: bytes) -> None: - self.key_id = key_id - self.algorithm = algorithm - self.plaintext = plaintext - - -class EncryptResult: - """The result of an encrypt operation. - - :param str key_id: The encryption key's Key Vault identifier - :param algorithm: The encryption algorithm used - :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm - :param bytes ciphertext: The encrypted bytes - - :keyword bytes iv: Initialization vector for symmetric algorithms - :keyword bytes authentication_tag: The tag to authenticate when performing decryption with an authenticated - algorithm - :keyword bytes additional_authenticated_data: Additional data to authenticate but not encrypt/decrypt when using an - authenticated algorithm - """ - - def __init__(self, key_id: Optional[str], algorithm: EncryptionAlgorithm, ciphertext: bytes, **kwargs: Any) -> None: - self.key_id = key_id - self.algorithm = algorithm - self.ciphertext = ciphertext - self.iv = kwargs.pop("iv", None) - self.tag = kwargs.pop("authentication_tag", None) - self.aad = kwargs.pop("additional_authenticated_data", None) - - -class SignResult: - """The result of a sign operation. - - :param str key_id: The signing key's Key Vault identifier - :param algorithm: The signature algorithm used - :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm - :param bytes signature: - """ - - def __init__(self, key_id: Optional[str], algorithm: SignatureAlgorithm, signature: bytes) -> None: - self.key_id = key_id - self.algorithm = algorithm - self.signature = signature - - -class VerifyResult: - """The result of a verify operation. - - :param str key_id: The signing key's Key Vault identifier - :param bool is_valid: Whether the signature is valid - :param algorithm: The signature algorithm used - :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm - """ - - def __init__(self, key_id: Optional[str], is_valid: bool, algorithm: SignatureAlgorithm) -> None: - self.key_id = key_id - self.is_valid = is_valid - self.algorithm = algorithm - - -class UnwrapResult: - """The result of an unwrap key operation. - - :param str key_id: Key encryption key's Key Vault identifier - :param algorithm: The key wrap algorithm used - :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm - :param bytes key: The unwrapped key - """ - - def __init__(self, key_id: Optional[str], algorithm: KeyWrapAlgorithm, key: bytes) -> None: - self.key_id = key_id - self.algorithm = algorithm - self.key = key - - -class WrapResult: - """The result of a wrap key operation. - - :param str key_id: The wrapping key's Key Vault identifier - :param algorithm: The key wrap algorithm used - :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm - :param bytes encrypted_key: The encrypted key bytes - """ - - def __init__(self, key_id: Optional[str], algorithm: KeyWrapAlgorithm, encrypted_key: bytes) -> None: - self.key_id = key_id - self.algorithm = algorithm - self.encrypted_key = encrypted_key diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py deleted file mode 100644 index 8c146d1a1fca..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import TYPE_CHECKING - -from .ec import EllipticCurveCryptographyProvider -from .local_provider import LocalCryptographyProvider -from .rsa import RsaCryptographyProvider -from .symmetric import SymmetricCryptographyProvider -from ... import KeyType - -if TYPE_CHECKING: - from ... import JsonWebKey - - -def get_local_cryptography_provider(key: "JsonWebKey") -> LocalCryptographyProvider: - if key.kty in (KeyType.ec, KeyType.ec_hsm): # type: ignore[attr-defined] - return EllipticCurveCryptographyProvider(key) - if key.kty in (KeyType.rsa, KeyType.rsa_hsm): # type: ignore[attr-defined] - return RsaCryptographyProvider(key) - if key.kty in (KeyType.oct, KeyType.oct_hsm): # type: ignore[attr-defined] - return SymmetricCryptographyProvider(key) - - raise ValueError(f'Unsupported key type "{key.kty}"') # type: ignore[attr-defined] - - -class NoLocalCryptography(LocalCryptographyProvider): - def __init__(self): # pylint:disable=super-init-not-called - return - - def supports(self, operation, algorithm): - return False - - def _get_internal_key(self, key): - return None diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py deleted file mode 100644 index d72dd505a1a2..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py +++ /dev/null @@ -1,34 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import TYPE_CHECKING - -from .local_provider import LocalCryptographyProvider -from .._internal import EllipticCurveKey -from ... import KeyOperation, KeyType - -if TYPE_CHECKING: - from .local_provider import Algorithm - from .._internal import Key - from ... import JsonWebKey - -_PRIVATE_KEY_OPERATIONS = frozenset((KeyOperation.decrypt, KeyOperation.sign, KeyOperation.unwrap_key)) - - -class EllipticCurveCryptographyProvider(LocalCryptographyProvider): - def _get_internal_key(self, key: "JsonWebKey") -> "Key": - if key.kty not in (KeyType.ec, KeyType.ec_hsm): # type: ignore[attr-defined] - raise ValueError('"key" must be an EC or EC-HSM key') - return EllipticCurveKey.from_jwk(key) - - def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: - if operation in _PRIVATE_KEY_OPERATIONS and not self._internal_key.is_private_key(): - return False - if operation in (KeyOperation.decrypt, KeyOperation.encrypt): - return algorithm in self._internal_key.supported_encryption_algorithms - if operation in (KeyOperation.unwrap_key, KeyOperation.wrap_key): - return algorithm in self._internal_key.supported_key_wrap_algorithms - if operation in (KeyOperation.sign, KeyOperation.verify): - return algorithm in self._internal_key.supported_signature_algorithms - return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py deleted file mode 100644 index 6e0edd2f526c..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py +++ /dev/null @@ -1,104 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import abc -import os -from typing import Optional, TYPE_CHECKING, Union - -from azure.core.exceptions import AzureError - -from .. import DecryptResult, EncryptResult, SignResult, UnwrapResult, VerifyResult, WrapResult -from ... import KeyOperation - -ABC = abc.ABC - -if TYPE_CHECKING: - from .._internal.key import Key - from .. import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm - from ... import JsonWebKey - - Algorithm = Union[EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm] - - -class LocalCryptographyProvider(ABC): - def __init__(self, key: "JsonWebKey") -> None: - self._allowed_ops = frozenset(key.key_ops or []) # type: ignore[attr-defined] - self._internal_key = self._get_internal_key(key) - self._key = key - - @abc.abstractmethod - def _get_internal_key(self, key: "JsonWebKey") -> "Key": - pass - - @abc.abstractmethod - def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: - pass - - @property - def key_id(self) -> "Optional[str]": - """The full identifier of the provider's key. - - :returns: The full identifier of the provider's key. - :rtype: str or None - """ - return self._key.kid # type: ignore[attr-defined] - - def _raise_if_unsupported(self, operation: KeyOperation, algorithm: "Algorithm") -> None: - if not self.supports(operation, algorithm): - raise NotImplementedError( - f'This key does not support the "{operation}" operation with algorithm "{algorithm}"' - ) - if operation not in self._allowed_ops: - raise AzureError(f'This key does not allow the "{operation}" operation') - - def encrypt( - self, algorithm: "EncryptionAlgorithm", plaintext: bytes, iv: "Optional[bytes]" = None - ) -> EncryptResult: - self._raise_if_unsupported(KeyOperation.encrypt, algorithm) - - # If an IV isn't provided with AES-CBCPAD encryption, try to create one - if iv is None and algorithm.value.endswith("CBCPAD"): - try: - iv = os.urandom(16) - except NotImplementedError as ex: - raise ValueError( - "An IV could not be generated on this OS. Please provide your own cryptographically random, " - "non-repeating IV for local cryptography." - ) from ex - - ciphertext = self._internal_key.encrypt(plaintext, algorithm=algorithm.value, iv=iv) - return EncryptResult( - key_id=self._key.kid, algorithm=algorithm, ciphertext=ciphertext, iv=iv # type: ignore[attr-defined] - ) - - def decrypt( - self, algorithm: "EncryptionAlgorithm", ciphertext: bytes, iv: "Optional[bytes]" = None - ) -> DecryptResult: - self._raise_if_unsupported(KeyOperation.decrypt, algorithm) - plaintext = self._internal_key.decrypt(ciphertext, iv=iv, algorithm=algorithm.value) - return DecryptResult( - key_id=self._key.kid, algorithm=algorithm, plaintext=plaintext # type: ignore[attr-defined] - ) - - def wrap_key(self, algorithm: "KeyWrapAlgorithm", key: bytes) -> "WrapResult": - self._raise_if_unsupported(KeyOperation.wrap_key, algorithm) - encrypted_key = self._internal_key.wrap_key(key, algorithm=algorithm.value) - return WrapResult( - key_id=self._key.kid, algorithm=algorithm, encrypted_key=encrypted_key # type: ignore[attr-defined] - ) - - def unwrap_key(self, algorithm: "KeyWrapAlgorithm", encrypted_key: bytes) -> "UnwrapResult": - self._raise_if_unsupported(KeyOperation.unwrap_key, algorithm) - unwrapped_key = self._internal_key.unwrap_key(encrypted_key, algorithm=algorithm.value) - return UnwrapResult(key_id=self._key.kid, algorithm=algorithm, key=unwrapped_key) # type: ignore[attr-defined] - - def sign(self, algorithm: "SignatureAlgorithm", digest: bytes) -> "SignResult": - self._raise_if_unsupported(KeyOperation.sign, algorithm) - signature = self._internal_key.sign(digest, algorithm=algorithm.value) - return SignResult(key_id=self._key.kid, algorithm=algorithm, signature=signature) # type: ignore[attr-defined] - - def verify(self, algorithm: "SignatureAlgorithm", digest: bytes, signature: bytes) -> "VerifyResult": - self._raise_if_unsupported(KeyOperation.verify, algorithm) - is_valid = self._internal_key.verify(digest, signature, algorithm=algorithm.value) - return VerifyResult(key_id=self._key.kid, algorithm=algorithm, is_valid=is_valid) # type: ignore[attr-defined] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py deleted file mode 100644 index 4394cc2a9b51..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py +++ /dev/null @@ -1,34 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import TYPE_CHECKING - -from .local_provider import LocalCryptographyProvider -from .._internal import RsaKey -from ... import KeyOperation, KeyType - -if TYPE_CHECKING: - from .local_provider import Algorithm - from .._internal import Key - from ... import JsonWebKey - -_PRIVATE_KEY_OPERATIONS = frozenset((KeyOperation.decrypt, KeyOperation.sign, KeyOperation.unwrap_key)) - - -class RsaCryptographyProvider(LocalCryptographyProvider): - def _get_internal_key(self, key: "JsonWebKey") -> "Key": - if key.kty not in (KeyType.rsa, KeyType.rsa_hsm): # type: ignore[attr-defined] - raise ValueError('"key" must be an RSA or RSA-HSM key') - return RsaKey.from_jwk(key) - - def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: - if operation in _PRIVATE_KEY_OPERATIONS and not self._internal_key.is_private_key(): - return False - if operation in (KeyOperation.decrypt, KeyOperation.encrypt): - return algorithm in self._internal_key.supported_encryption_algorithms - if operation in (KeyOperation.unwrap_key, KeyOperation.wrap_key): - return algorithm in self._internal_key.supported_key_wrap_algorithms - if operation in (KeyOperation.sign, KeyOperation.verify): - return algorithm in self._internal_key.supported_signature_algorithms - return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py deleted file mode 100644 index 3a5f473b36c1..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py +++ /dev/null @@ -1,28 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import TYPE_CHECKING - -from .local_provider import LocalCryptographyProvider -from .._internal import SymmetricKey -from ... import KeyOperation, KeyType - -if TYPE_CHECKING: - from .local_provider import Algorithm - from .._internal import Key - from ... import JsonWebKey - - -class SymmetricCryptographyProvider(LocalCryptographyProvider): - def _get_internal_key(self, key: "JsonWebKey") -> "Key": - if key.kty not in (KeyType.oct, KeyType.oct_hsm): # type: ignore[attr-defined] - raise ValueError('"key" must be an oct or oct-HSM (symmetric) key') - return SymmetricKey.from_jwk(key) - - def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: - if operation in (KeyOperation.decrypt, KeyOperation.encrypt): - return algorithm in self._internal_key.supported_encryption_algorithms - if operation in (KeyOperation.unwrap_key, KeyOperation.wrap_key): - return algorithm in self._internal_key.supported_key_wrap_algorithms - return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py deleted file mode 100644 index 3a8c0f5ee127..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from typing import Any, List, Optional - -from ._client import CryptographyClient - - -__all__ = [ - "CryptographyClient", -] - - -def __dir__() -> List[str]: - return __all__ - - -# Allow importing these types for backwards compatibility, but exclude indexing types that shouldn't be in aio namespace - - -def __getattr__(name: str): - requested: Optional[Any] = None - if name == "EncryptionAlgorithm": - from .. import EncryptionAlgorithm - - requested = EncryptionAlgorithm - if name == "KeyWrapAlgorithm": - from .. import KeyWrapAlgorithm - - requested = KeyWrapAlgorithm - if name == "SignatureAlgorithm": - from .. import SignatureAlgorithm - - requested = SignatureAlgorithm - if name == "EncryptResult": - from .. import EncryptResult - - requested = EncryptResult - if name == "SignResult": - from .. import SignResult - - requested = SignResult - if name == "WrapResult": - from .. import WrapResult - - requested = WrapResult - if requested: - return requested - raise AttributeError(f"module 'azure.keyvault.keys.crypto.aio' has no attribute {name}") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py deleted file mode 100644 index 13111932ebbe..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py +++ /dev/null @@ -1,503 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from datetime import datetime -import logging -from typing import Any, cast, Dict, Optional, Union - -from azure.core.credentials_async import AsyncTokenCredential -from azure.core.exceptions import HttpResponseError -from azure.core.tracing.decorator_async import distributed_trace_async - -from .. import ( - DecryptResult, - EncryptionAlgorithm, - EncryptResult, - KeyWrapAlgorithm, - SignatureAlgorithm, - SignResult, - VerifyResult, - UnwrapResult, - WrapResult, -) -from .._client import _validate_arguments -from .._key_validity import raise_if_time_invalid -from .._providers import get_local_cryptography_provider, NoLocalCryptography -from ... import KeyOperation -from ..._models import JsonWebKey, KeyVaultKey -from ..._shared import AsyncKeyVaultClientBase, KeyVaultResourceId, parse_key_vault_id - -_LOGGER = logging.getLogger(__name__) - - -class CryptographyClient(AsyncKeyVaultClientBase): - """Performs cryptographic operations using Azure Key Vault keys. - - This client will perform operations locally when it's intialized with the necessary key material or is able to get - that material from Key Vault. When the required key material is unavailable, cryptographic operations are performed - by the Key Vault service. - - :param key: Either a azure.keyvault.keys.KeyVaultKey instance as returned by - :func:`~azure.keyvault.keys.aio.KeyClient.get_key`, or a string. - If a string, the value must be the identifier of an Azure Key Vault key. Including a version is recommended. - :type key: str or azure.keyvault.keys.KeyVaultKey - :param credential: An object which can provide an access token for the vault, such as a credential from - :mod:`azure.identity.aio` - :type credential: ~azure.core.credentials_async.AsyncTokenCredential - - :keyword api_version: Version of the service API to use. Defaults to the most recent. - :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str - :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key - Vault or Managed HSM domain. Defaults to True. - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START create_client] - :end-before: [END create_client] - :caption: Create a CryptographyClient - :language: python - :dedent: 8 - """ - - # pylint:disable=protected-access - - def __init__(self, key: Union[KeyVaultKey, str], credential: AsyncTokenCredential, **kwargs: Any) -> None: - self._jwk = kwargs.pop("_jwk", False) - self._not_before: Optional[datetime] = None - self._expires_on: Optional[datetime] = None - self._key_id: Optional[KeyVaultResourceId] = None - - if isinstance(key, KeyVaultKey): - self._key: Union[JsonWebKey, KeyVaultKey, str, None] = key.key - self._key_id = parse_key_vault_id(key.id) - if key.properties._attributes: - self._not_before = key.properties.not_before - self._expires_on = key.properties.expires_on - elif isinstance(key, str): - self._key = None - self._key_id = parse_key_vault_id(key) - if self._key_id.version is None: - self._key_id.version = "" # to avoid an error and get the latest version when getting the key - self._keys_get_forbidden = False - elif self._jwk: - self._key = key - else: - raise ValueError("'key' must be a KeyVaultKey instance or a key ID string") - - if self._jwk: - try: - self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) - self._initialized = True - except Exception as ex: - raise ValueError("The provided jwk is not valid for local cryptography") from ex - else: - self._local_provider = NoLocalCryptography() - self._initialized = False - - self._vault_url = None if (self._jwk or self._key_id is None) else self._key_id.vault_url # type: ignore - super().__init__(vault_url=self._vault_url or "vault_url", credential=credential, **kwargs) - - @property - def key_id(self) -> Optional[str]: - """The full identifier of the client's key. - - This property may be None when a client is constructed with :func:`from_jwk`. - - :returns: The full identifier of the client's key. - :rtype: str or None - """ - if not self._jwk: - return self._key_id.source_id if self._key_id else None - return cast(JsonWebKey, self._key).kid # type: ignore[attr-defined] - - @property - def vault_url(self) -> Optional[str]: # type: ignore - """The base vault URL of the client's key. - - This property may be None when a client is constructed with :func:`from_jwk`. - - :returns: The base vault URL of the client's key. - :rtype: str or None - """ - return self._vault_url - - @classmethod - def from_jwk(cls, jwk: Union[JsonWebKey, Dict[str, Any]]) -> "CryptographyClient": - """Creates a client that can only perform cryptographic operations locally. - - :param jwk: the key's cryptographic material, as a JsonWebKey or dictionary. - :type jwk: JsonWebKey or Dict[str, Any] - - :returns: A client that can only perform local cryptographic operations. - :rtype: CryptographyClient - """ - if not isinstance(jwk, JsonWebKey): - jwk = JsonWebKey(**jwk) - return cls(jwk, object(), _jwk=True) # type: ignore - - @distributed_trace_async - async def _initialize(self, **kwargs: Any) -> None: - if self._initialized: - return - - # try to get the key material, if we don't have it and aren't forbidden to do so - if not (self._key or self._keys_get_forbidden): - try: - key_bundle = await self._client.get_key( - self._key_id.name if self._key_id else None, - self._key_id.version if self._key_id else None, - **kwargs, - ) - key = KeyVaultKey._from_key_bundle(key_bundle) - self._key = key.key - self._key_id = parse_key_vault_id(key.id) # update the key ID in case we didn't have the version before - except HttpResponseError as ex: - # if we got a 403, we don't have keys/get permission and won't try to get the key again - # (other errors may be transient) - self._keys_get_forbidden = ex.status_code == 403 - - # if we have the key material, create a local crypto provider with it - if self._key: - self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) - self._initialized = True - else: - # try to get the key again next time unless we know we're forbidden to do so - self._initialized = self._keys_get_forbidden - - @distributed_trace_async - async def encrypt( - self, - algorithm: EncryptionAlgorithm, - plaintext: bytes, - *, - iv: Optional[bytes] = None, - additional_authenticated_data: Optional[bytes] = None, - **kwargs: Any, - ) -> EncryptResult: - """Encrypt bytes using the client's key. - - Requires the keys/encrypt permission. This method encrypts only a single block of data, whose size depends on - the key and encryption algorithm. - - :param algorithm: Encryption algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm - :param bytes plaintext: Bytes to encrypt - - :keyword iv: Initialization vector. Required for only AES-CBC(PAD) encryption. If you pass your own IV, - make sure you use a cryptographically random, non-repeating IV. If omitted, an attempt will be made to - generate an IV via `os.urandom `_ for local - cryptography; for remote cryptography, Key Vault will generate an IV. - :paramtype iv: bytes or None - :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use - with AES-GCM encryption. - :paramtype additional_authenticated_data: bytes or None - - :returns: The result of the encryption operation. - :rtype: ~azure.keyvault.keys.crypto.EncryptResult - - :raises ValueError: if parameters that are incompatible with the specified algorithm are provided, or if - generating an IV fails on the current platform. - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START encrypt] - :end-before: [END encrypt] - :caption: Encrypt bytes - :language: python - :dedent: 8 - """ - _validate_arguments( - operation=KeyOperation.encrypt, algorithm=algorithm, iv=iv, aad=additional_authenticated_data - ) - await self._initialize(**kwargs) - - if self._local_provider.supports(KeyOperation.encrypt, algorithm): - raise_if_time_invalid(self._not_before, self._expires_on) - try: - return self._local_provider.encrypt(algorithm, plaintext, iv=iv) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local encrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.encrypt}" operation with algorithm "{algorithm}"' - ) - - operation_result = await self._client.encrypt( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters( - algorithm=algorithm, value=plaintext, iv=iv, aad=additional_authenticated_data - ), - **kwargs, - ) - - result_iv = operation_result.iv if hasattr(operation_result, "iv") else None - result_tag = operation_result.authentication_tag if hasattr(operation_result, "authentication_tag") else None - result_aad = ( - operation_result.additional_authenticated_data - if hasattr(operation_result, "additional_authenticated_data") - else None - ) - - return EncryptResult( - key_id=self.key_id, - algorithm=algorithm, - ciphertext=operation_result.result, - iv=result_iv, - authentication_tag=result_tag, - additional_authenticated_data=result_aad, - ) - - @distributed_trace_async - async def decrypt( - self, - algorithm: EncryptionAlgorithm, - ciphertext: bytes, - *, - iv: Optional[bytes] = None, - authentication_tag: Optional[bytes] = None, - additional_authenticated_data: Optional[bytes] = None, - **kwargs: Any, - ) -> DecryptResult: - """Decrypt a single block of encrypted data using the client's key. - - Requires the keys/decrypt permission. This method decrypts only a single block of data, whose size depends on - the key and encryption algorithm. - - :param algorithm: Encryption algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm - :param bytes ciphertext: Encrypted bytes to decrypt. Microsoft recommends you not use CBC without first ensuring - the integrity of the ciphertext using, for example, an HMAC. See - https://learn.microsoft.com/dotnet/standard/security/vulnerabilities-cbc-mode for more information. - - :keyword iv: The initialization vector used during encryption. Required for AES decryption. - :paramtype iv: bytes or None - :keyword authentication_tag: The authentication tag generated during encryption. Required for only AES-GCM - decryption. - :paramtype authentication_tag: bytes or None - :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use - with AES-GCM decryption. - :paramtype additional_authenticated_data: bytes or None - - :returns: The result of the decryption operation. - :rtype: ~azure.keyvault.keys.crypto.DecryptResult - - :raises ValueError: If parameters that are incompatible with the specified algorithm are provided. - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START decrypt] - :end-before: [END decrypt] - :caption: Decrypt bytes - :language: python - :dedent: 8 - """ - _validate_arguments( - operation=KeyOperation.decrypt, - algorithm=algorithm, - iv=iv, - tag=authentication_tag, - aad=additional_authenticated_data, - ) - await self._initialize(**kwargs) - - if self._local_provider.supports(KeyOperation.decrypt, algorithm): - try: - return self._local_provider.decrypt(algorithm, ciphertext, iv=iv) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local decrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.decrypt}" operation with algorithm "{algorithm}"' - ) - - operation_result = await self._client.decrypt( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters( - algorithm=algorithm, value=ciphertext, iv=iv, tag=authentication_tag, aad=additional_authenticated_data - ), - **kwargs, - ) - - return DecryptResult(key_id=self.key_id, algorithm=algorithm, plaintext=operation_result.result) - - @distributed_trace_async - async def wrap_key(self, algorithm: KeyWrapAlgorithm, key: bytes, **kwargs: Any) -> WrapResult: - """Wrap a key with the client's key. - - Requires the keys/wrapKey permission. - - :param algorithm: wrapping algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm - :param bytes key: key to wrap - - :returns: The result of the wrapping operation. - :rtype: ~azure.keyvault.keys.crypto.WrapResult - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START wrap_key] - :end-before: [END wrap_key] - :caption: Wrap a key - :language: python - :dedent: 8 - """ - await self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.wrap_key, algorithm): - raise_if_time_invalid(self._not_before, self._expires_on) - try: - return self._local_provider.wrap_key(algorithm, key) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local wrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.wrap_key}" operation with algorithm "{algorithm}"' - ) - - operation_result = await self._client.wrap_key( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=key), - **kwargs, - ) - - return WrapResult(key_id=self.key_id, algorithm=algorithm, encrypted_key=operation_result.result) - - @distributed_trace_async - async def unwrap_key(self, algorithm: KeyWrapAlgorithm, encrypted_key: bytes, **kwargs: Any) -> UnwrapResult: - """Unwrap a key previously wrapped with the client's key. - - Requires the keys/unwrapKey permission. - - :param algorithm: wrapping algorithm to use - :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm - :param bytes encrypted_key: the wrapped key - - :returns: The result of the unwrapping operation. - :rtype: ~azure.keyvault.keys.crypto.UnwrapResult - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START unwrap_key] - :end-before: [END unwrap_key] - :caption: Unwrap a key - :language: python - :dedent: 8 - """ - await self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.unwrap_key, algorithm): - try: - return self._local_provider.unwrap_key(algorithm, encrypted_key) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local unwrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.unwrap_key}" operation with algorithm "{algorithm}"' - ) - - operation_result = await self._client.unwrap_key( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=encrypted_key), - **kwargs, - ) - - return UnwrapResult(key_id=self.key_id, algorithm=algorithm, key=operation_result.result) - - @distributed_trace_async - async def sign(self, algorithm: SignatureAlgorithm, digest: bytes, **kwargs: Any) -> SignResult: - """Create a signature from a digest using the client's key. - - Requires the keys/sign permission. - - :param algorithm: signing algorithm - :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm - :param bytes digest: hashed bytes to sign - - :returns: The result of the signing operation. - :rtype: ~azure.keyvault.keys.crypto.SignResult - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START sign] - :end-before: [END sign] - :caption: Sign bytes - :language: python - :dedent: 8 - """ - await self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.sign, algorithm): - raise_if_time_invalid(self._not_before, self._expires_on) - try: - return self._local_provider.sign(algorithm, digest) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local sign operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.sign}" operation with algorithm "{algorithm}"' - ) - - operation_result = await self._client.sign( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeySignParameters(algorithm=algorithm, value=digest), - **kwargs, - ) - - return SignResult(key_id=self.key_id, algorithm=algorithm, signature=operation_result.result) - - @distributed_trace_async - async def verify( - self, algorithm: SignatureAlgorithm, digest: bytes, signature: bytes, **kwargs: Any - ) -> VerifyResult: - """Verify a signature using the client's key. - - Requires the keys/verify permission. - - :param algorithm: verification algorithm - :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm - :param bytes digest: Pre-hashed digest corresponding to **signature**. The hash algorithm used must be - compatible with ``algorithm``. - :param bytes signature: signature to verify - - :returns: The result of the verifying operation. - :rtype: ~azure.keyvault.keys.crypto.VerifyResult - - .. literalinclude:: ../tests/test_examples_crypto_async.py - :start-after: [START verify] - :end-before: [END verify] - :caption: Verify a signature - :language: python - :dedent: 8 - """ - await self._initialize(**kwargs) - if self._local_provider.supports(KeyOperation.verify, algorithm): - try: - return self._local_provider.verify(algorithm, digest, signature) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.warning("Local verify operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) - if self._jwk: - raise - elif self._jwk: - raise NotImplementedError( - f'This key does not support the "{KeyOperation.verify}" operation with algorithm "{algorithm}"' - ) - - operation_result = await self._client.verify( - key_name=self._key_id.name if self._key_id else None, - key_version=self._key_id.version if self._key_id else None, - parameters=self._models.KeyVerifyParameters(algorithm=algorithm, digest=digest, signature=signature), - **kwargs, - ) - - return VerifyResult(key_id=self.key_id, algorithm=algorithm, is_valid=operation_result.value) - - async def __aenter__(self) -> "CryptographyClient": - await self._client.__aenter__() - return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/py.typed b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/py.typed deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/sdk/keyvault/azure-keyvault-keys/pyproject.toml b/sdk/keyvault/azure-keyvault-keys/pyproject.toml index 5da059fdcf7e..aa0744fc3775 100644 --- a/sdk/keyvault/azure-keyvault-keys/pyproject.toml +++ b/sdk/keyvault/azure-keyvault-keys/pyproject.toml @@ -1,18 +1,14 @@ -# -------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# Code generated by Microsoft (R) Python Code Generator. -# Changes may cause incorrect behavior and will be lost if the code is regenerated. -# -------------------------------------------------------------------------- - [build-system] -requires = ["setuptools>=77.0.3", "wheel"] +requires = [ + "setuptools>=77.0.3", + "wheel", +] build-backend = "setuptools.build_meta" [project] name = "azure-keyvault-keys" authors = [ - { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" }, + { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" }, ] description = "Microsoft Corporation Azure Key Vault Keys Client Library for Python" license = "MIT" @@ -21,15 +17,16 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -requires-python = ">=3.9" -keywords = ["azure", "azure sdk"] - +requires-python = ">=3.10" +keywords = [ + "azure", + "azure sdk", +] dependencies = [ "isodate>=0.6.1", "azure-core>=1.37.0", @@ -37,15 +34,22 @@ dependencies = [ "cryptography>=44.0.2", ] dynamic = [ -"version", "readme" + "version", + "readme", ] [project.urls] repository = "https://github.com/Azure/azure-sdk-for-python" -[tool.setuptools.dynamic] -version = {attr = "azure.keyvault.keys._version.VERSION"} -readme = {file = ["README.md", "CHANGELOG.md"], content-type = "text/markdown"} +[tool.setuptools.dynamic.version] +attr = "azure.keyvault.keys._generated._version.VERSION" + +[tool.setuptools.dynamic.readme] +file = [ + "README.md", + "CHANGELOG.md", +] +content-type = "text/markdown" [tool.setuptools.packages.find] exclude = [ @@ -56,10 +60,13 @@ exclude = [ "doc*", "azure", "azure.keyvault", + "azure.keyvault.keys", ] [tool.setuptools.package-data] -pytyped = ["py.typed"] +pytyped = [ + "py.typed", +] [tool.azure-sdk-build] pyright = false @@ -67,3 +74,6 @@ pyright = false [tool.azure-sdk-conda] in_bundle = true bundle_name = "azure-keyvault" + +[packaging] +auto_update = false diff --git a/sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml b/sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml deleted file mode 100644 index e7687fdae93b..000000000000 --- a/sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml +++ /dev/null @@ -1,2 +0,0 @@ -[packaging] -auto_update = false \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py b/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py index 4cc9668b3b8b..6663690fc1ca 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py @@ -12,7 +12,6 @@ from azure.keyvault.keys._shared.client_base import ApiVersion from devtools_testutils import AzureRecordedTestCase - HSM_UNSUPPORTED_VERSIONS = {ApiVersion.V2016_10_01, ApiVersion.V7_0, ApiVersion.V7_1} diff --git a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py index 5f987a254b83..89a32415e45a 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py @@ -13,7 +13,6 @@ from azure.keyvault.keys.crypto.aio import CryptographyClient as AsyncCryptographyClient from azure.mgmt.keyvault.models import KeyPermissions, Permissions - # without keys/get, a CryptographyClient created with a key ID performs all ops remotely NO_GET = Permissions(keys=[p.value for p in KeyPermissions if p.value != "get"]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py index 469de42d8a35..36676296424d 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py @@ -14,7 +14,6 @@ from azure.keyvault.keys.crypto.aio import CryptographyClient as AsyncCryptographyClient from azure.mgmt.keyvault.models import KeyPermissions, Permissions - # without keys/get, a CryptographyClient created with a key ID performs all ops remotely NO_GET = Permissions(keys=[p.value for p in KeyPermissions if p.value != "get"]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py index 2b09561f154b..9df7fc252c22 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py @@ -13,7 +13,6 @@ from azure.keyvault.keys.crypto.aio import CryptographyClient as AsyncCryptographyClient from azure.mgmt.keyvault.models import KeyPermissions, Permissions - # without keys/get, a CryptographyClient created with a key ID performs all ops remotely NO_GET = Permissions(keys=[p.value for p in KeyPermissions if p.value != "get"]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py index 5c32754a86ee..f5a680f27674 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py @@ -6,6 +6,7 @@ Tests for the HTTP challenge authentication implementation. These tests aren't parallelizable, because the challenge cache is global to the process. """ + import base64 import functools from itertools import product diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py index ab1e6af8c3d6..d70f2f9fc7c4 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py @@ -6,6 +6,7 @@ Tests for the HTTP challenge authentication implementation. These tests aren't parallelizable, because the challenge cache is global to the process. """ + import asyncio import functools from itertools import product diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py index 382bee35dfb7..86f900f9f61a 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -46,7 +46,6 @@ from _test_case import KeysClientPreparer, get_decorator from _keys_test_case import KeysTestCase - all_api_versions = get_decorator() only_hsm = get_decorator(only_hsm=True) only_vault_default = get_decorator(only_vault=True, api_versions=[DEFAULT_VERSION]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py index d101f0507fe1..c2b665882c82 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py @@ -32,7 +32,6 @@ from _shared.test_case_async import KeyVaultTestCase from _keys_test_case import KeysTestCase - all_api_versions = get_decorator(is_async=True) only_hsm = get_decorator(only_hsm=True, is_async=True) only_vault_7_4_plus = get_decorator(only_vault=True, is_async=True, api_versions=[ApiVersion.V7_4, ApiVersion.V7_5]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py index fe21f207819b..71866fef987e 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py @@ -35,7 +35,6 @@ from devtools_testutils.aio import recorded_by_proxy_async from _keys_test_case import KeysTestCase - all_api_versions = get_decorator(is_async=True) only_hsm = get_decorator(only_hsm=True, is_async=True) only_hsm_default = get_decorator(only_hsm=True, is_async=True, api_versions=[DEFAULT_VERSION]) diff --git a/sdk/keyvault/azure-keyvault-keys/tsp-location.yaml b/sdk/keyvault/azure-keyvault-keys/tsp-location.yaml index d837ec69a67b..9107ab8f5a5b 100644 --- a/sdk/keyvault/azure-keyvault-keys/tsp-location.yaml +++ b/sdk/keyvault/azure-keyvault-keys/tsp-location.yaml @@ -1,3 +1,5 @@ directory: specification/keyvault/data-plane/Keys -commit: f6bd06be22baf3a18504ffef0f590230850953e5 +commit: 9dd262c196cd5d27bb01a6bf4d645d9c89cc0f02 repo: Azure/azure-rest-api-specs +additionalDirectories: +- specification/keyvault/data-plane/Keys/common From 1f87279d815249006049048dec22fc03c563f6ab Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Wed, 20 May 2026 11:34:50 -0700 Subject: [PATCH 2/6] Restore handwritten files, update changelog for 2026-03-01-preview API version --- sdk/keyvault/azure-keyvault-keys/CHANGELOG.md | 10 +- sdk/keyvault/azure-keyvault-keys/MANIFEST.in | 2 +- .../azure/keyvault/keys/__init__.py | 44 +- .../azure/keyvault/keys/_client.py | 1010 ++++++++++++++++ .../azure/keyvault/keys/_enums.py | 72 ++ .../azure/keyvault/keys/_models.py | 659 +++++++++++ .../azure/keyvault/keys/_sdk_moniker.py | 7 + .../azure/keyvault/keys/_shared/__init__.py | 78 ++ .../azure/keyvault/keys/_shared/_polling.py | 136 +++ .../keyvault/keys/_shared/_polling_async.py | 80 ++ .../_shared/async_challenge_auth_policy.py | 256 +++++ .../keys/_shared/async_client_base.py | 117 ++ .../keys/_shared/challenge_auth_policy.py | 270 +++++ .../keyvault/keys/_shared/client_base.py | 162 +++ .../keyvault/keys/_shared/http_challenge.py | 186 +++ .../keys/_shared/http_challenge_cache.py | 93 ++ .../azure/keyvault/keys/_version.py | 6 + .../azure/keyvault/keys/aio/__init__.py | 7 + .../azure/keyvault/keys/aio/_client.py | 1017 +++++++++++++++++ .../azure/keyvault/keys/crypto/__init__.py | 32 + .../azure/keyvault/keys/crypto/_client.py | 577 ++++++++++ .../azure/keyvault/keys/crypto/_enums.py | 67 ++ .../keys/crypto/_internal/__init__.py | 32 + .../keys/crypto/_internal/_internal.py | 131 +++ .../keys/crypto/_internal/algorithm.py | 78 ++ .../crypto/_internal/algorithms/__init__.py | 38 + .../crypto/_internal/algorithms/aes_cbc.py | 145 +++ .../_internal/algorithms/aes_cbc_hmac.py | 149 +++ .../crypto/_internal/algorithms/aes_kw.py | 68 ++ .../keys/crypto/_internal/algorithms/ecdsa.py | 60 + .../_internal/algorithms/rsa_encryption.py | 79 ++ .../_internal/algorithms/rsa_signing.py | 75 ++ .../keys/crypto/_internal/algorithms/sha_2.py | 53 + .../keyvault/keys/crypto/_internal/ec_key.py | 106 ++ .../keyvault/keys/crypto/_internal/key.py | 94 ++ .../keyvault/keys/crypto/_internal/rsa_key.py | 228 ++++ .../keys/crypto/_internal/symmetric_key.py | 125 ++ .../keys/crypto/_internal/transform.py | 61 + .../keyvault/keys/crypto/_key_validity.py | 16 + .../azure/keyvault/keys/crypto/_models.py | 621 ++++++++++ .../keys/crypto/_providers/__init__.py | 36 + .../keyvault/keys/crypto/_providers/ec.py | 34 + .../keys/crypto/_providers/local_provider.py | 104 ++ .../keyvault/keys/crypto/_providers/rsa.py | 34 + .../keys/crypto/_providers/symmetric.py | 28 + .../keyvault/keys/crypto/aio/__init__.py | 50 + .../azure/keyvault/keys/crypto/aio/_client.py | 503 ++++++++ .../azure/keyvault/keys/py.typed | 0 .../azure-keyvault-keys/pyproject.toml | 46 +- .../azure-keyvault-keys/sdk_packaging.toml | 2 + .../azure-keyvault-keys/tests/_test_case.py | 1 + .../tests/perfstress_tests/decrypt.py | 1 + .../tests/perfstress_tests/sign.py | 1 + .../tests/perfstress_tests/unwrap.py | 1 + .../tests/test_challenge_auth.py | 1 - .../tests/test_challenge_auth_async.py | 1 - .../tests/test_crypto_client.py | 1 + .../tests/test_crypto_client_async.py | 1 + .../tests/test_keys_async.py | 1 + 59 files changed, 7859 insertions(+), 34 deletions(-) create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py create mode 100644 sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/py.typed create mode 100644 sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml diff --git a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md index f912b86e1c50..d92d874c5806 100644 --- a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md @@ -1,8 +1,14 @@ # Release History -## 4.12.0b1 (2026-05-20) +## 4.12.0b1 (Unreleased) -skip changelog generation for data-plane package and please add changelog manually. +### Features Added + +- Added support for service API version `2026-03-01-preview` + +### Other Changes + +- Key Vault API version `2026-03-01-preview` is now the default ## 4.11.1 (2026-05-18) diff --git a/sdk/keyvault/azure-keyvault-keys/MANIFEST.in b/sdk/keyvault/azure-keyvault-keys/MANIFEST.in index 7696bd6b2f38..9bd7188bf0b2 100644 --- a/sdk/keyvault/azure-keyvault-keys/MANIFEST.in +++ b/sdk/keyvault/azure-keyvault-keys/MANIFEST.in @@ -1,6 +1,6 @@ include *.md include LICENSE -include azure/keyvault/keys/_generated/py.typed +include azure/keyvault/keys/py.typed recursive-include tests *.py recursive-include samples *.py *.md include azure/__init__.py diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py index d55ccad1f573..3a06bca6b656 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/__init__.py @@ -1 +1,43 @@ -__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------- +from ._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation, KeyRotationPolicyAction, KeyType +from ._shared.client_base import ApiVersion +from ._models import ( + DeletedKey, + JsonWebKey, + KeyAttestation, + KeyProperties, + KeyReleasePolicy, + KeyRotationLifetimeAction, + KeyRotationPolicy, + KeyVaultKey, + KeyVaultKeyIdentifier, + ReleaseKeyResult, +) +from ._client import KeyClient + +__all__ = [ + "ApiVersion", + "KeyClient", + "JsonWebKey", + "KeyAttestation", + "KeyVaultKey", + "KeyVaultKeyIdentifier", + "KeyCurveName", + "KeyExportEncryptionAlgorithm", + "KeyOperation", + "KeyRotationPolicyAction", + "KeyType", + "DeletedKey", + "KeyProperties", + "KeyReleasePolicy", + "KeyRotationLifetimeAction", + "KeyRotationPolicy", + "ReleaseKeyResult", +] + +from ._version import VERSION + +__version__ = VERSION diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py new file mode 100644 index 000000000000..b1c2eb3a4b93 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_client.py @@ -0,0 +1,1010 @@ +# pylint: disable=too-many-lines +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from datetime import datetime +from functools import partial +from typing import Any, Dict, List, Optional, Union + +from azure.core.paging import ItemPaged +from azure.core.polling import LROPoller +from azure.core.tracing.decorator import distributed_trace + +from .crypto import CryptographyClient +from ._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation, KeyType +from ._generated.models import KeyAttributes +from ._models import JsonWebKey, KeyRotationLifetimeAction +from ._shared import KeyVaultClientBase +from ._shared._polling import DeleteRecoverPollingMethod, KeyVaultOperationPoller +from ._models import DeletedKey, KeyVaultKey, KeyProperties, KeyReleasePolicy, KeyRotationPolicy, ReleaseKeyResult + + +def _get_key_id(vault_url, key_name, version=None): + without_version = f"{vault_url}/keys/{key_name}" + return without_version + "/" + version if version else without_version + + +class KeyClient(KeyVaultClientBase): + """A high-level interface for managing a vault's keys. + + :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". + You should validate that this URL references a valid Key Vault or Managed HSM resource. + See https://aka.ms/azsdk/blog/vault-uri for details. + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity` + :type credential: ~azure.core.credentials.TokenCredential + + :keyword api_version: Version of the service API to use. Defaults to the most recent. + :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str + :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key + Vault or Managed HSM domain. Defaults to True. + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START create_key_client] + :end-before: [END create_key_client] + :language: python + :caption: Create a new ``KeyClient`` + :dedent: 4 + """ + + # pylint:disable=protected-access, too-many-public-methods + + def _get_attributes( + self, + enabled: Optional[bool], + not_before: Optional[datetime], + expires_on: Optional[datetime], + exportable: Optional[bool] = None, + ) -> Optional[KeyAttributes]: + """Return a KeyAttributes object if non-None attributes are provided, or None otherwise. + + :param enabled: Whether the key is enabled. + :type enabled: bool or None + :param not_before: Not before date of the key in UTC. + :type not_before: ~datetime.datetime or None + :param expires_on: Expiry date of the key in UTC. + :type expires_on: ~datetime.datetime or None + :param exportable: Whether the private key can be exported. + :type exportable: bool or None + + :returns: An autorest-generated model of the key's attributes. + :rtype: KeyAttributes + """ + if enabled is not None or not_before is not None or expires_on is not None or exportable is not None: + return self._models.KeyAttributes( + enabled=enabled, not_before=not_before, expires=expires_on, exportable=exportable + ) + return None + + def get_cryptography_client( + self, + key_name: str, + *, + key_version: Optional[str] = None, + **kwargs, # pylint: disable=unused-argument + ) -> CryptographyClient: + """Gets a :class:`~azure.keyvault.keys.crypto.CryptographyClient` for the given key. + + :param str key_name: The name of the key used to perform cryptographic operations. + + :keyword key_version: Optional version of the key used to perform cryptographic operations. + :paramtype key_version: str or None + + :returns: A :class:`~azure.keyvault.keys.crypto.CryptographyClient` using the same options, credentials, and + HTTP client as this :class:`~azure.keyvault.keys.KeyClient`. + :rtype: ~azure.keyvault.keys.crypto.CryptographyClient + """ + key_id = _get_key_id(self._vault_url, key_name, key_version) + + # We provide a fake credential because the generated client already has the KeyClient's real credential + return CryptographyClient( + key_id, object(), generated_client=self._client, generated_models=self._models # type: ignore + ) + + @distributed_trace + def create_key( + self, + name: str, + key_type: Union[str, KeyType], + *, + size: Optional[int] = None, + curve: Optional[Union[str, KeyCurveName]] = None, + public_exponent: Optional[int] = None, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a key or, if ``name`` is already in use, create a new version of the key. + + Requires keys/create permission. + + :param str name: The name of the new key. + :param key_type: The type of key to create + :type key_type: ~azure.keyvault.keys.KeyType or str + + :keyword size: Key size in bits. Applies only to RSA and symmetric keys. Consider using + :func:`create_rsa_key` or :func:`create_oct_key` instead. + :paramtype size: int or None + :keyword curve: Elliptic curve name. Applies only to elliptic curve keys. Defaults to the NIST P-256 + elliptic curve. To create an elliptic curve key, consider using :func:`create_ec_key` instead. + :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None + :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. + :paramtype public_exponent: int or None + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START create_key] + :end-before: [END create_key] + :language: python + :caption: Create a key + :dedent: 8 + """ + attributes = self._get_attributes( + enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable + ) + + policy = release_policy + if policy is not None: + policy = self._models.KeyReleasePolicy( + encoded_policy=policy.encoded_policy, content_type=policy.content_type, immutable=policy.immutable + ) + parameters = self._models.KeyCreateParameters( + kty=key_type, + key_size=size, + key_attributes=attributes, + key_ops=key_operations, + tags=tags, + curve=curve, + public_exponent=public_exponent, + release_policy=policy, + ) + + bundle = self._client.create_key(key_name=name, parameters=parameters, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace + def create_rsa_key( + self, + name: str, + *, + size: Optional[int] = None, + public_exponent: Optional[int] = None, + hardware_protected: Optional[bool] = False, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a new RSA key or, if ``name`` is already in use, create a new version of the key + + Requires the keys/create permission. + + :param str name: The name for the new key. + + :keyword size: Key size in bits, for example 2048, 3072, or 4096. + :paramtype size: int or None + :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. + :paramtype public_exponent: int or None + :keyword hardware_protected: Whether the key should be created in a hardware security module. + Defaults to ``False``. + :paramtype hardware_protected: bool or None + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START create_rsa_key] + :end-before: [END create_rsa_key] + :language: python + :caption: Create RSA key + :dedent: 8 + """ + return self.create_key( + name, + key_type="RSA-HSM" if hardware_protected else "RSA", + size=size, + public_exponent=public_exponent, + key_operations=key_operations, + enabled=enabled, + tags=tags, + not_before=not_before, + expires_on=expires_on, + exportable=exportable, + release_policy=release_policy, + **kwargs, + ) + + @distributed_trace + def create_ec_key( + self, + name: str, + *, + curve: Optional[Union[str, KeyCurveName]] = None, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + hardware_protected: Optional[bool] = False, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a new elliptic curve key or, if ``name`` is already in use, create a new version of the key. + + Requires the keys/create permission. + + :param str name: The name for the new key. + + :keyword curve: Elliptic curve name. Defaults to the NIST P-256 elliptic curve. + :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword hardware_protected: Whether the key should be created in a hardware security module. + Defaults to ``False``. + :paramtype hardware_protected: bool or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START create_ec_key] + :end-before: [END create_ec_key] + :language: python + :caption: Create an elliptic curve key + :dedent: 8 + """ + return self.create_key( + name, + key_type="EC-HSM" if hardware_protected else "EC", + curve=curve, + key_operations=key_operations, + enabled=enabled, + tags=tags, + not_before=not_before, + expires_on=expires_on, + exportable=exportable, + release_policy=release_policy, + **kwargs, + ) + + @distributed_trace + def create_oct_key( + self, + name: str, + *, + size: Optional[int] = None, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + hardware_protected: Optional[bool] = False, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a new octet sequence (symmetric) key or, if ``name`` is in use, create a new version of the key. + + Requires the keys/create permission. + + :param str name: The name for the new key. + + :keyword size: Key size in bits, for example 128, 192, or 256. + :paramtype size: int or None + :keyword key_operations: Allowed key operations. + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword hardware_protected: Whether the key should be created in a hardware security module. + Defaults to ``False``. + :paramtype hardware_protected: bool or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START create_oct_key] + :end-before: [END create_oct_key] + :language: python + :caption: Create an octet sequence (symmetric) key + :dedent: 8 + """ + return self.create_key( + name, + key_type="oct-HSM" if hardware_protected else "oct", + size=size, + key_operations=key_operations, + enabled=enabled, + tags=tags, + not_before=not_before, + expires_on=expires_on, + exportable=exportable, + release_policy=release_policy, + **kwargs, + ) + + @distributed_trace + def begin_delete_key( # pylint:disable=bad-option-value,delete-operation-wrong-return-type + self, name: str, **kwargs: Any + ) -> LROPoller[DeletedKey]: + """Delete all versions of a key and its cryptographic material. + + Requires keys/delete permission. When this method returns Key Vault has begun deleting the key. Deletion may + take several seconds in a vault with soft-delete enabled. This method therefore returns a poller enabling you to + wait for deletion to complete. + + :param str name: The name of the key to delete. + + :returns: A poller for the delete key operation. The poller's `result` method returns the + :class:`~azure.keyvault.keys.DeletedKey` without waiting for deletion to complete. If the vault has + soft-delete enabled and you want to permanently delete the key with :func:`purge_deleted_key`, call the + poller's `wait` method first. It will block until the deletion is complete. The `wait` method requires + keys/get permission. + :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.keys.DeletedKey] + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START delete_key] + :end-before: [END delete_key] + :language: python + :caption: Delete a key + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, deleted_key_bundle = self._client.delete_key( + key_name=name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + deleted_key = DeletedKey._from_deleted_key_bundle(deleted_key_bundle) + + command = partial(self.get_deleted_key, name=name, **kwargs) + polling_method = DeleteRecoverPollingMethod( + # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished + finished=deleted_key.recovery_id is None, + pipeline_response=pipeline_response, + command=command, + final_resource=deleted_key, + interval=polling_interval, + ) + return KeyVaultOperationPoller(polling_method) + + @distributed_trace + def get_key(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: + """Get a key's attributes and, if it's an asymmetric key, its public material. + + Requires keys/get permission. + + :param str name: The name of the key to get. + :param version: (optional) A specific version of the key to get. If not specified, gets the latest version + of the key. + :type version: str or None + + :returns: The fetched key. + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START get_key] + :end-before: [END get_key] + :language: python + :caption: Get a key + :dedent: 8 + """ + bundle = self._client.get_key(name, key_version=version or "", **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace + def get_deleted_key(self, name: str, **kwargs: Any) -> DeletedKey: + """Get a deleted key. Possible only in a vault with soft-delete enabled. + + Requires keys/get permission. + + :param str name: The name of the key + + :returns: The deleted key + :rtype: ~azure.keyvault.keys.DeletedKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START get_deleted_key] + :end-before: [END get_deleted_key] + :language: python + :caption: Get a deleted key + :dedent: 8 + """ + bundle = self._client.get_deleted_key(name, **kwargs) + return DeletedKey._from_deleted_key_bundle(bundle) + + @distributed_trace + def list_deleted_keys(self, **kwargs: Any) -> ItemPaged[DeletedKey]: + """List all deleted keys, including the public part of each. Possible only in a vault with soft-delete enabled. + + Requires keys/list permission. + + :returns: An iterator of deleted keys + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.keys.DeletedKey] + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START list_deleted_keys] + :end-before: [END list_deleted_keys] + :language: python + :caption: List all the deleted keys + :dedent: 8 + """ + return self._client.get_deleted_keys( + maxresults=kwargs.pop("max_page_size", None), + cls=lambda objs: [DeletedKey._from_deleted_key_item(x) for x in objs], + **kwargs, + ) + + @distributed_trace + def list_properties_of_keys(self, **kwargs: Any) -> ItemPaged[KeyProperties]: + """List identifiers and properties of all keys in the vault. + + Requires keys/list permission. + + :returns: An iterator of keys without their cryptographic material or version information + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.keys.KeyProperties] + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START list_keys] + :end-before: [END list_keys] + :language: python + :caption: List all keys + :dedent: 8 + """ + return self._client.get_keys( + maxresults=kwargs.pop("max_page_size", None), + cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], + **kwargs, + ) + + @distributed_trace + def list_properties_of_key_versions(self, name: str, **kwargs: Any) -> ItemPaged[KeyProperties]: + """List the identifiers and properties of a key's versions. + + Requires keys/list permission. + + :param str name: The name of the key + + :returns: An iterator of keys without their cryptographic material + :rtype: ~azure.core.paging.ItemPaged[~azure.keyvault.keys.KeyProperties] + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START list_properties_of_key_versions] + :end-before: [END list_properties_of_key_versions] + :language: python + :caption: List all versions of a key + :dedent: 8 + """ + return self._client.get_key_versions( + name, + maxresults=kwargs.pop("max_page_size", None), + cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], + **kwargs, + ) + + @distributed_trace + def purge_deleted_key(self, name: str, **kwargs: Any) -> None: + """Permanently deletes a deleted key. Only possible in a vault with soft-delete enabled. + + Performs an irreversible deletion of the specified key, without possibility for recovery. The operation is not + available if the :py:attr:`~azure.keyvault.keys.KeyProperties.recovery_level` does not specify 'Purgeable'. + This method is only necessary for purging a key before its + :py:attr:`~azure.keyvault.keys.DeletedKey.scheduled_purge_date`. + + Requires keys/purge permission. + + :param str name: The name of the deleted key to purge + + :returns: None + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # if the vault has soft-delete enabled, purge permanently deletes a deleted key + # (with soft-delete disabled, begin_delete_key is permanent) + key_client.purge_deleted_key("key-name") + + """ + self._client.purge_deleted_key(key_name=name, **kwargs) + + @distributed_trace + def begin_recover_deleted_key(self, name: str, **kwargs: Any) -> LROPoller[KeyVaultKey]: + """Recover a deleted key to its latest version. Possible only in a vault with soft-delete enabled. + + Requires keys/recover permission. + + When this method returns Key Vault has begun recovering the key. Recovery may take several seconds. This + method therefore returns a poller enabling you to wait for recovery to complete. Waiting is only necessary when + you want to use the recovered key in another operation immediately. + + :param str name: The name of the deleted key to recover + + :returns: A poller for the recovery operation. The poller's `result` method returns the recovered + :class:`~azure.keyvault.keys.KeyVaultKey` without waiting for recovery to complete. If you want to use the + recovered key immediately, call the poller's `wait` method, which blocks until the key is ready to use. The + `wait` method requires keys/get permission. + :rtype: ~azure.core.polling.LROPoller[~azure.keyvault.keys.KeyVaultKey] + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START recover_deleted_key] + :end-before: [END recover_deleted_key] + :language: python + :caption: Recover a deleted key + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, recovered_key_bundle = self._client.recover_deleted_key( + key_name=name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + recovered_key = KeyVaultKey._from_key_bundle(recovered_key_bundle) + command = partial(self.get_key, name=name, **kwargs) + polling_method = DeleteRecoverPollingMethod( + finished=False, + pipeline_response=pipeline_response, + command=command, + final_resource=recovered_key, + interval=polling_interval, + ) + + return KeyVaultOperationPoller(polling_method) + + @distributed_trace + def update_key_properties( + self, + name: str, + version: Optional[str] = None, + *, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Change a key's properties (not its cryptographic material). + + Requires keys/update permission. + + :param str name: The name of key to update + :param version: (optional) The version of the key to update. If unspecified, the latest version is updated. + :type version: str or None + + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The updated key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START update_key] + :end-before: [END update_key] + :language: python + :caption: Update a key's attributes + :dedent: 8 + """ + attributes = self._get_attributes(enabled=enabled, not_before=not_before, expires_on=expires_on) + + policy = release_policy + if policy is not None: + policy = self._models.KeyReleasePolicy( + content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable + ) + parameters = self._models.KeyUpdateParameters( + key_ops=key_operations, + key_attributes=attributes, + tags=tags, + release_policy=policy, + ) + + bundle = self._client.update_key(name, key_version=version or "", parameters=parameters, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace + def backup_key(self, name: str, **kwargs: Any) -> bytes: + """Back up a key in a protected form useable only by Azure Key Vault. + + Requires keys/backup permission. + + This is intended to allow copying a key from one vault to another. Both vaults must be owned by the same Azure + subscription. Also, backup / restore cannot be performed across geopolitical boundaries. For example, a backup + from a vault in a USA region cannot be restored to a vault in an EU region. + + :param str name: The name of the key to back up + + :returns: The key backup result, in a protected bytes format that can only be used by Azure Key Vault. + :rtype: bytes + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START backup_key] + :end-before: [END backup_key] + :language: python + :caption: Get a key backup + :dedent: 8 + """ + backup_result = self._client.backup_key(name, **kwargs) + return backup_result.value + + @distributed_trace + def restore_key_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultKey: + """Restore a key backup to the vault. + + Requires keys/restore permission. + + This imports all versions of the key, with its name, attributes, and access control policies. If the key's name + is already in use, restoring it will fail. Also, the target vault must be owned by the same Microsoft Azure + subscription as the source vault. + + :param bytes backup: A key backup as returned by :func:`backup_key` + + :returns: The restored key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.ResourceExistsError or ~azure.core.exceptions.HttpResponseError: + the former if the backed up key's name is already in use; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys.py + :start-after: [START restore_key_backup] + :end-before: [END restore_key_backup] + :language: python + :caption: Restore a key backup + :dedent: 8 + """ + bundle = self._client.restore_key( + parameters=self._models.KeyRestoreParameters(key_bundle_backup=backup), **kwargs + ) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace + def import_key( + self, + name: str, + key: JsonWebKey, + *, + hardware_protected: Optional[bool] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Import a key created externally. + + Requires keys/import permission. If ``name`` is already in use, the key will be imported as a new version. + + :param str name: Name for the imported key + :param key: The JSON web key to import + :type key: ~azure.keyvault.keys.JsonWebKey + + :keyword hardware_protected: Whether the key should be backed by a hardware security module + :paramtype hardware_protected: bool or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The imported key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + """ + attributes = self._get_attributes( + enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable + ) + + policy = release_policy + if policy is not None: + policy = self._models.KeyReleasePolicy( + content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable + ) + parameters = self._models.KeyImportParameters( + key=key._to_generated_model(), + key_attributes=attributes, + hsm=hardware_protected, + tags=tags, + release_policy=policy, + ) + + bundle = self._client.import_key(name, parameters=parameters, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace + def release_key( + self, + name: str, + target_attestation_token: str, + *, + version: Optional[str] = None, + algorithm: Optional[Union[str, KeyExportEncryptionAlgorithm]] = None, + nonce: Optional[str] = None, + **kwargs: Any, + ) -> ReleaseKeyResult: + """Releases a key. + + The release key operation is applicable to all key types. The target key must be marked + exportable. This operation requires the keys/release permission. + + :param str name: The name of the key to get. + :param str target_attestation_token: The attestation assertion for the target of the key release. + + :keyword version: A specific version of the key to release. If unspecified, the latest version is released. + :paramtype version: str or None + :keyword algorithm: The encryption algorithm to use to protect the released key material. + :paramtype algorithm: str or ~azure.keyvault.keys.KeyExportEncryptionAlgorithm or None + :keyword nonce: A client-provided nonce for freshness. + :paramtype nonce: str or None + + :return: The result of the key release. + :rtype: ~azure.keyvault.keys.ReleaseKeyResult + + :raises ~azure.core.exceptions.HttpResponseError: + """ + result = self._client.release( + key_name=name, + key_version=version or "", + parameters=self._models.KeyReleaseParameters( + target_attestation_token=target_attestation_token, + nonce=nonce, + enc=algorithm, + ), + **kwargs, + ) + return ReleaseKeyResult(result.value) + + @distributed_trace + def get_random_bytes(self, count: int, **kwargs: Any) -> bytes: + """Get the requested number of random bytes from a managed HSM. + + :param int count: The requested number of random bytes. + + :return: The random bytes. + :rtype: bytes + + :raises ValueError or ~azure.core.exceptions.HttpResponseError: + the former if less than one random byte is requested; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_key_client.py + :start-after: [START get_random_bytes] + :end-before: [END get_random_bytes] + :language: python + :caption: Get random bytes + :dedent: 12 + """ + if count < 1: + raise ValueError("At least one random byte must be requested") + parameters = self._models.GetRandomBytesRequest(count=count) + result = self._client.get_random_bytes(parameters=parameters, **kwargs) + return result.value + + @distributed_trace + def get_key_rotation_policy(self, key_name: str, **kwargs: Any) -> KeyRotationPolicy: + """Get the rotation policy of a Key Vault key. + + :param str key_name: The name of the key. + + :return: The key rotation policy. + :rtype: ~azure.keyvault.keys.KeyRotationPolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + policy = self._client.get_key_rotation_policy(key_name=key_name, **kwargs) + return KeyRotationPolicy._from_generated(policy) + + @distributed_trace + def rotate_key(self, name: str, **kwargs: Any) -> KeyVaultKey: + """Rotate the key based on the key policy by generating a new version of the key. + + This operation requires the keys/rotate permission. + + :param str name: The name of the key to rotate. + + :return: The new version of the rotated key. + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = self._client.rotate_key(key_name=name, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace + def update_key_rotation_policy( # pylint: disable=unused-argument + self, + key_name: str, + policy: KeyRotationPolicy, + *, + lifetime_actions: Optional[List[KeyRotationLifetimeAction]] = None, + expires_in: Optional[str] = None, + **kwargs: Any, + ) -> KeyRotationPolicy: + """Updates the rotation policy of a Key Vault key. + + This operation requires the keys/update permission. + + :param str key_name: The name of the key in the given vault. + :param policy: The new rotation policy for the key. + :type policy: ~azure.keyvault.keys.KeyRotationPolicy + + :keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key. This will + override the lifetime actions of the provided ``policy``. + :paramtype lifetime_actions: List[~azure.keyvault.keys.KeyRotationLifetimeAction] + :keyword str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an + ISO 8601 duration. For example: 90 days is "P90D", 3 months is "P3M", and 48 hours is "PT48H". See + `Wikipedia `_ for more information on ISO 8601 durations. + This will override the expiry time of the provided ``policy``. + + :return: The updated rotation policy. + :rtype: ~azure.keyvault.keys.KeyRotationPolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + actions = lifetime_actions or policy.lifetime_actions + if actions: + actions = [ + self._models.LifetimeActions( + action=self._models.LifetimeActionsType(type=action.action), + trigger=self._models.LifetimeActionsTrigger( + time_after_create=action.time_after_create, time_before_expiry=action.time_before_expiry + ), + ) + for action in actions + ] + + attributes = self._models.KeyRotationPolicyAttributes(expiry_time=expires_in or policy.expires_in) + new_policy = self._models.KeyRotationPolicy(lifetime_actions=actions or [], attributes=attributes) + result = self._client.update_key_rotation_policy(key_name=key_name, key_rotation_policy=new_policy) + return KeyRotationPolicy._from_generated(result) + + @distributed_trace + def get_key_attestation(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: + """Get a key and its attestation blob. + + This method is applicable to any key stored in Azure Key Vault Managed HSM. This operation requires the keys/get + permission. + + :param str name: The name of the key. + :param version: (optional) A specific version of the key to get. If not specified, gets the latest version + of the key. + :type version: str or None + + :return: The key attestation. + :rtype: ~azure.keyvault.keys.KeyAttestation + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = self._client.get_key_attestation(key_name=name, key_version=version or "", **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + def __enter__(self) -> "KeyClient": + self._client.__enter__() + return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py new file mode 100644 index 000000000000..24dab8ff5ad7 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_enums.py @@ -0,0 +1,72 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +# pylint: disable=enum-must-be-uppercase + +from enum import Enum + +from azure.core import CaseInsensitiveEnumMeta + + +class KeyCurveName(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Supported elliptic curves""" + + p_256 = "P-256" #: The NIST P-256 elliptic curve, AKA SECG curve SECP256R1. + p_384 = "P-384" #: The NIST P-384 elliptic curve, AKA SECG curve SECP384R1. + p_521 = "P-521" #: The NIST P-521 elliptic curve, AKA SECG curve SECP521R1. + p_256_k = "P-256K" #: The SECG SECP256K1 elliptic curve. + + +class KeyExportEncryptionAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Supported algorithms for protecting exported key material""" + + ckm_rsa_aes_key_wrap = "CKM_RSA_AES_KEY_WRAP" + rsa_aes_key_wrap_256 = "RSA_AES_KEY_WRAP_256" + rsa_aes_key_wrap_384 = "RSA_AES_KEY_WRAP_384" + + +class KeyOperation(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Supported key operations""" + + encrypt = "encrypt" + decrypt = "decrypt" + import_key = "import" + sign = "sign" + verify = "verify" + wrap_key = "wrapKey" + unwrap_key = "unwrapKey" + export = "export" + + +class KeyRotationPolicyAction(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """The action that will be executed in a key rotation policy""" + + rotate = "Rotate" #: Rotate the key based on the key policy. + notify = "Notify" #: Trigger Event Grid events. + + @classmethod + def _missing_(cls, value): + for member in cls: + if member.value.lower() == value.lower(): + return member + raise ValueError(f"{value} is not a valid KeyRotationPolicyAction") + + +class KeyType(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Supported key types""" + + ec = "EC" #: Elliptic Curve + ec_hsm = "EC-HSM" #: Elliptic Curve with a private key which is not exportable from the HSM + rsa = "RSA" #: RSA (https://tools.ietf.org/html/rfc3447) + rsa_hsm = "RSA-HSM" #: RSA with a private key which is not exportable from the HSM + oct = "oct" #: Octet sequence (used to represent symmetric keys) + oct_hsm = "oct-HSM" #: Octet sequence with a private key which is not exportable from the HSM + + @classmethod + def _missing_(cls, value): + for member in cls: + if member.value.lower() == value.lower(): + return member + raise ValueError(f"{value} is not a valid KeyType") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py new file mode 100644 index 000000000000..fb4255c78129 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_models.py @@ -0,0 +1,659 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------- +from collections import namedtuple +from datetime import datetime +from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING + +from ._enums import KeyOperation, KeyRotationPolicyAction, KeyType +from ._shared import parse_key_vault_id +from ._generated.models import JsonWebKey as _JsonWebKey + +if TYPE_CHECKING: + from ._generated import models as _models + +KeyOperationResult = namedtuple("KeyOperationResult", ["id", "value"]) + + +class JsonWebKey(object): + """As defined in http://tools.ietf.org/html/draft-ietf-jose-json-web-key-18. All parameters are optional. + + :keyword str kid: Key identifier. + :keyword kty: Key Type (kty), as defined in https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 + :paramtype kty: ~azure.keyvault.keys.KeyType or str + :keyword key_ops: Allowed operations for the key + :paramtype key_ops: list[str or ~azure.keyvault.keys.KeyOperation] + :keyword bytes n: RSA modulus. + :keyword bytes e: RSA public exponent. + :keyword bytes d: RSA private exponent, or the D component of an EC private key. + :keyword bytes dp: RSA private key parameter. + :keyword bytes dq: RSA private key parameter. + :keyword bytes qi: RSA private key parameter. + :keyword bytes p: RSA secret prime. + :keyword bytes q: RSA secret prime, with p < q. + :keyword bytes k: Symmetric key. + :keyword bytes t: HSM Token, used with 'Bring Your Own Key'. + :keyword crv: Elliptic curve name. + :paramtype crv: ~azure.keyvault.keys.KeyCurveName or str + :keyword bytes x: X component of an EC public key. + :keyword bytes y: Y component of an EC public key. + """ + + _FIELDS = ("kid", "kty", "key_ops", "n", "e", "d", "dp", "dq", "qi", "p", "q", "k", "t", "crv", "x", "y") + + def __init__(self, **kwargs: Any) -> None: + for field in self._FIELDS: + setattr(self, field, kwargs.get(field)) + + def _to_generated_model(self) -> _JsonWebKey: + jwk = _JsonWebKey() + for field in self._FIELDS: + setattr(jwk, field, getattr(self, field)) + return jwk + + +class KeyAttestation: + """The key attestation information. + + :ivar certificate_pem_file: The certificate used for attestation validation, in PEM format. + :vartype certificate_pem_file: bytes or None + :ivar private_key_attestation: The key attestation corresponding to the private key material of the key. + :vartype private_key_attestation: bytes or None + :ivar public_key_attestation: The key attestation corresponding to the public key material of the key. + :vartype public_key_attestation: bytes or None + :ivar version: The version of the attestation. + :vartype version: str or None + """ + + def __init__( + self, + *, + certificate_pem_file: Optional[bytes] = None, + private_key_attestation: Optional[bytes] = None, + public_key_attestation: Optional[bytes] = None, + version: Optional[str] = None, + ) -> None: + self.certificate_pem_file = certificate_pem_file + self.private_key_attestation = private_key_attestation + self.public_key_attestation = public_key_attestation + self.version = version + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_generated(cls, attestation: "_models.KeyAttestation") -> "KeyAttestation": + return cls( + certificate_pem_file=attestation.certificate_pem_file, + private_key_attestation=attestation.private_key_attestation, + public_key_attestation=attestation.public_key_attestation, + version=attestation.version, + ) + + +class KeyProperties(object): + """A key's ID and attributes. + + :param str key_id: The key ID. + :param attributes: The key attributes. + :type attributes: ~azure.keyvault.keys._generated.models.KeyAttributes + + :keyword bool managed: Whether the key's lifetime is managed by Key Vault. + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword release_policy: The azure.keyvault.keys.KeyReleasePolicy specifying the rules under which the key + can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + """ + + def __init__(self, key_id: str, attributes: "Optional[_models.KeyAttributes]" = None, **kwargs: Any) -> None: + self._attributes = attributes + self._id = key_id + self._vault_id = KeyVaultKeyIdentifier(key_id) + self._managed = kwargs.get("managed", None) + self._tags = kwargs.get("tags", None) + self._release_policy = kwargs.pop("release_policy", None) + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_key_bundle(cls, key_bundle: Union["_models.KeyBundle", "_models.DeletedKeyBundle"]) -> "KeyProperties": + # pylint:disable=line-too-long + # release_policy was added in 7.3-preview + release_policy = None + if ( + hasattr(key_bundle, "release_policy") and key_bundle.release_policy is not None # type: ignore[attr-defined] + ): + release_policy = KeyReleasePolicy( + encoded_policy=key_bundle.release_policy.encoded_policy, # type: ignore + content_type=key_bundle.release_policy.content_type, # type: ignore[attr-defined] + immutable=key_bundle.release_policy.immutable, # type: ignore[attr-defined] + ) + + return cls( + key_bundle.key.kid, # type: ignore + attributes=key_bundle.attributes, + managed=key_bundle.managed, + tags=key_bundle.tags, + release_policy=release_policy, + ) + + @classmethod + def _from_key_item(cls, key_item: Union["_models.KeyItem", "_models.DeletedKeyItem"]) -> "KeyProperties": + return cls( + key_id=key_item.kid, # type: ignore + attributes=key_item.attributes, + managed=key_item.managed, + tags=key_item.tags, + ) + + @property + def id(self) -> str: + """The key ID. + + :returns: The key ID. + :rtype: str + """ + return self._id + + @property + def name(self) -> str: + """The key name. + + :returns: The key name. + :rtype: str + """ + return self._vault_id.name + + @property + def version(self) -> Optional[str]: + """The key version. + + :returns: The key version. + :rtype: str or None + """ + return self._vault_id.version + + @property + def enabled(self) -> Optional[bool]: + """Whether the key is enabled for use. + + :returns: True if the key is enabled for use; False otherwise. + :rtype: bool or None + """ + return self._attributes.enabled if self._attributes else None + + @property + def not_before(self) -> Optional[datetime]: + """The time before which the key can not be used, in UTC. + + :returns: The time before which the key can not be used, in UTC. + :rtype: ~datetime.datetime or None + """ + return self._attributes.not_before if self._attributes else None + + @property + def expires_on(self) -> Optional[datetime]: + """When the key will expire, in UTC. + + :returns: When the key will expire, in UTC. + :rtype: ~datetime.datetime or None + """ + return self._attributes.expires if self._attributes else None + + @property + def created_on(self) -> Optional[datetime]: + """When the key was created, in UTC. + + :returns: When the key was created, in UTC. + :rtype: ~datetime.datetime or None + """ + return self._attributes.created if self._attributes else None + + @property + def updated_on(self) -> Optional[datetime]: + """When the key was last updated, in UTC. + + :returns: When the key was last updated, in UTC. + :rtype: ~datetime.datetime or None + """ + return self._attributes.updated if self._attributes else None + + @property + def vault_url(self) -> str: + """URL of the vault containing the key. + + :returns: URL of the vault containing the key. + :rtype: str + """ + return self._vault_id.vault_url + + @property + def recoverable_days(self) -> Optional[int]: + """The number of days the key is retained before being deleted from a soft-delete enabled Key Vault. + + :returns: The number of days the key is retained before being deleted from a soft-delete enabled Key Vault. + :rtype: int or None + """ + # recoverable_days was added in 7.1-preview + if self._attributes: + return getattr(self._attributes, "recoverable_days", None) + return None + + @property + def recovery_level(self) -> Optional[str]: + """The vault's deletion recovery level for keys. + + :returns: The vault's deletion recovery level for keys. + :rtype: str or None + """ + return self._attributes.recovery_level if self._attributes else None + + @property + def tags(self) -> Optional[Dict[str, str]]: + """Application specific metadata in the form of key-value pairs. + + :returns: A dictionary of tags attached to the key. + :rtype: dict[str, str] or None + """ + return self._tags + + @property + def managed(self) -> Optional[bool]: + """Whether the key's lifetime is managed by Key Vault. If the key backs a certificate, this will be true. + + :returns: True if the key's lifetime is managed by Key Vault; False otherwise. + :rtype: bool or None + """ + return self._managed + + @property + def exportable(self) -> Optional[bool]: + """Whether the private key can be exported. + + :returns: True if the private key can be exported; False otherwise. + :rtype: bool or None + """ + # exportable was added in 7.3-preview + if self._attributes: + return getattr(self._attributes, "exportable", None) + return None + + @property + def release_policy(self) -> "Optional[KeyReleasePolicy]": + """The :class:`~azure.keyvault.keys.KeyReleasePolicy` specifying the rules under which the key can be exported. + + :returns: The key's release policy specifying the rules for exporting. + :rtype: ~azure.keyvault.keys.KeyReleasePolicy or None + """ + return self._release_policy + + @property + def hsm_platform(self) -> Optional[str]: + """The underlying HSM platform. + + :returns: The underlying HSM platform. + :rtype: str or None + """ + # hsm_platform was added in 7.5-preview.1 + if self._attributes: + return getattr(self._attributes, "hsm_platform", None) + return None + + @property + def attestation(self) -> Optional[KeyAttestation]: + """The key attestation, if available and requested. + + :returns: The key or key version attestation information. + :rtype: ~azure.keyvault.keys.KeyAttestation or None + """ + # attestation was added in 7.6-preview.2 + if self._attributes: + attestation = getattr(self._attributes, "attestation", None) + if attestation: + return KeyAttestation._from_generated(attestation=attestation) # pylint:disable=protected-access + return None + + +class KeyReleasePolicy(object): + """The policy rules under which a key can be exported. + + :param bytes encoded_policy: The policy rules under which the key can be released. Encoded based on the + ``content_type``. For more information regarding release policy grammar, please refer to: + https://aka.ms/policygrammarkeys for Azure Key Vault; https://aka.ms/policygrammarmhsm for Azure Managed HSM. + + :keyword str content_type: Content type and version of the release policy. Defaults to "application/json; + charset=utf-8" if omitted. + :keyword bool immutable: Marks a release policy as immutable. An immutable release policy cannot be changed or + updated after being marked immutable. Release policies are mutable by default. + """ + + def __init__(self, encoded_policy: bytes, **kwargs: Any) -> None: + self.encoded_policy = encoded_policy + self.content_type = kwargs.get("content_type", None) + self.immutable = kwargs.get("immutable", None) + + +class ReleaseKeyResult(object): + """The result of a key release operation. + + :ivar str value: A signed token containing the released key. + + :param str value: A signed token containing the released key. + """ + + def __init__(self, value: str) -> None: + self.value = value + + +class KeyRotationLifetimeAction(object): + """An action and its corresponding trigger that will be performed by Key Vault over the lifetime of a key. + + :param action: The action that will be executed. + :type action: ~azure.keyvault.keys.KeyRotationPolicyAction or str + + :keyword time_after_create: Time after creation to attempt the specified action, as an ISO 8601 duration. + For example, 90 days is "P90D". See `Wikipedia `_ for more + information on ISO 8601 durations. + :paramtype time_after_create: str or None + :keyword time_before_expiry: Time before expiry to attempt the specified action, as an ISO 8601 duration. + For example, 90 days is "P90D". See `Wikipedia `_ for more + information on ISO 8601 durations. + :paramtype time_before_expiry: str or None + """ + + def __init__(self, action: Union[KeyRotationPolicyAction, str], **kwargs: Any) -> None: + self.action = action + self.time_after_create: Optional[str] = kwargs.get("time_after_create", None) + self.time_before_expiry: Optional[str] = kwargs.get("time_before_expiry", None) + + @classmethod + def _from_generated(cls, lifetime_action: "_models.LifetimeActions") -> "KeyRotationLifetimeAction": + if lifetime_action.action: + if lifetime_action.trigger: + return cls( + action=lifetime_action.action.type, # type: ignore + time_after_create=lifetime_action.trigger.time_after_create, + time_before_expiry=lifetime_action.trigger.time_before_expiry, + ) + return cls(action=lifetime_action.action) # type: ignore + raise ValueError("Provided LifetimeActions model is missing a required lifetime action property.") + + +class KeyRotationPolicy(object): + """The key rotation policy that belongs to a key. + + :ivar id: The identifier of the key rotation policy. + :vartype id: str or None + :ivar lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key. + :vartype lifetime_actions: list[~azure.keyvault.keys.KeyRotationLifetimeAction] + :ivar expires_in: The expiry time of the policy that will be applied on new key versions, defined as an ISO 8601 + duration. For example, 90 days is "P90D". See `Wikipedia `_ for + more information on ISO 8601 durations. + :vartype expires_in: str or None + :ivar created_on: When the policy was created, in UTC + :vartype created_on: ~datetime.datetime or None + :ivar updated_on: When the policy was last updated, in UTC + :vartype updated_on: ~datetime.datetime or None + """ + + def __init__(self, **kwargs: Any) -> None: + self.id = kwargs.get("policy_id", None) + self.lifetime_actions: List[KeyRotationLifetimeAction] = kwargs.get("lifetime_actions", []) + self.expires_in = kwargs.get("expires_in", None) + self.created_on = kwargs.get("created_on", None) + self.updated_on = kwargs.get("updated_on", None) + + @classmethod + def _from_generated(cls, policy: "_models.KeyRotationPolicy") -> "KeyRotationPolicy": + lifetime_actions = ( + [] + if policy.lifetime_actions is None + else [ + KeyRotationLifetimeAction._from_generated(action) # pylint:disable=protected-access + for action in policy.lifetime_actions + ] + ) + if policy.attributes: + return cls( + policy_id=policy.id, + lifetime_actions=lifetime_actions, + expires_in=policy.attributes.expiry_time, + created_on=policy.attributes.created, + updated_on=policy.attributes.updated, + ) + return cls(policy_id=policy.id, lifetime_actions=lifetime_actions) + + +class KeyVaultKey(object): + """A key's attributes and cryptographic material. + + :param str key_id: Key Vault's identifier for the key. Typically a URI, e.g. + https://myvault.vault.azure.net/keys/my-key/version + :param jwk: The key's cryptographic material as a JSON Web Key (https://tools.ietf.org/html/rfc7517). This may be + provided as a dictionary or keyword arguments. See :class:`~azure.keyvault.keys.models.JsonWebKey` for field + names. + :type jwk: Dict[str, Any] + + Providing cryptographic material as keyword arguments: + + .. code-block:: python + + from azure.keyvault.keys.models import KeyVaultKey + + key_id = 'https://myvault.vault.azure.net/keys/my-key/my-key-version' + key_bytes = os.urandom(32) + key = KeyVaultKey(key_id, k=key_bytes, kty='oct', key_ops=['unwrapKey', 'wrapKey']) + + Providing cryptographic material as a dictionary: + + .. code-block:: python + + from azure.keyvault.keys.models import KeyVaultKey + + key_id = 'https://myvault.vault.azure.net/keys/my-key/my-key-version' + key_bytes = os.urandom(32) + jwk = {'k': key_bytes, 'kty': 'oct', 'key_ops': ['unwrapKey', 'wrapKey']} + key = KeyVaultKey(key_id, jwk=jwk) + + """ + + def __init__(self, key_id: str, jwk: Optional[Dict[str, Any]] = None, **kwargs) -> None: + self._properties: KeyProperties = kwargs.pop("properties", None) or KeyProperties(key_id, **kwargs) + if isinstance(jwk, dict): + if any(field in kwargs for field in JsonWebKey._FIELDS): + raise ValueError( + "Individual keyword arguments for key material and the 'jwk' argument are mutually exclusive." + ) + self._key_material = JsonWebKey(**jwk) + else: + self._key_material = JsonWebKey(**kwargs) + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_key_bundle(cls, key_bundle: "_models.KeyBundle") -> "KeyVaultKey": + # pylint:disable=protected-access + return cls( + key_id=key_bundle.key.kid, # type: ignore + jwk={field: getattr(key_bundle.key, field, None) for field in JsonWebKey._FIELDS}, + properties=KeyProperties._from_key_bundle(key_bundle), + ) + + @property + def id(self) -> str: + """The key ID. + + :returns: The key ID. + :rtype: str + """ + return self._properties.id + + @property + def name(self) -> str: + """The key name. + + :returns: The key name. + :rtype: str + """ + return self._properties.name + + @property + def properties(self) -> KeyProperties: + """The key properties. + + :returns: The key properties. + :rtype: ~azure.keyvault.keys.KeyProperties + """ + return self._properties + + @property + def key(self) -> JsonWebKey: + """The JSON Web Key (JWK) for the key. + + :returns: The JSON Web Key (JWK) for the key. + :rtype: ~azure.keyvault.keys.JsonWebKey + """ + return self._key_material + + @property + def key_type(self) -> Union[str, KeyType]: + """The key's type. See :class:`~azure.keyvault.keys.KeyType` for possible values. + + :returns: The key's type. See :class:`~azure.keyvault.keys.KeyType` for possible values. + :rtype: ~azure.keyvault.keys.KeyType or str + """ + # pylint:disable=no-member + return self._key_material.kty # type: ignore[attr-defined] + + @property + def key_operations(self) -> List[Union[str, KeyOperation]]: + """Permitted operations. See :class:`~azure.keyvault.keys.KeyOperation` for possible values. + + :returns: Permitted operations. See :class:`~azure.keyvault.keys.KeyOperation` for possible values. + :rtype: List[~azure.keyvault.keys.KeyOperation or str] + """ + # pylint:disable=no-member + return self._key_material.key_ops # type: ignore[attr-defined] + + +class KeyVaultKeyIdentifier(object): + """Information about a KeyVaultKey parsed from a key ID. + + :param str source_id: The full original identifier of a key + + :raises ValueError: if the key ID is improperly formatted + + Example: + .. literalinclude:: ../tests/test_parse_id.py + :start-after: [START parse_key_vault_key_id] + :end-before: [END parse_key_vault_key_id] + :language: python + :caption: Parse a key's ID + :dedent: 8 + """ + + def __init__(self, source_id: str) -> None: + self._resource_id = parse_key_vault_id(source_id) + + @property + def source_id(self) -> str: + return self._resource_id.source_id + + @property + def vault_url(self) -> str: + return self._resource_id.vault_url + + @property + def name(self) -> str: + return self._resource_id.name + + @property + def version(self) -> Optional[str]: + return self._resource_id.version + + +class DeletedKey(KeyVaultKey): + """A deleted key's properties, cryptographic material and its deletion information. + + If soft-delete is enabled, returns information about its recovery as well. + + :param properties: Properties of the deleted key. + :type properties: ~azure.keyvault.keys.KeyProperties + :param deleted_date: When the key was deleted, in UTC. + :type deleted_date: ~datetime.datetime or None + :param recovery_id: An identifier used to recover the deleted key. Returns ``None`` if soft-delete is disabled. + :type recovery_id: str or None + :param scheduled_purge_date: When the key is scheduled to be purged, in UTC. Returns ``None`` if soft-delete is + disabled. + :type scheduled_purge_date: ~datetime.datetime or None + """ + + def __init__( + self, + properties: KeyProperties, + deleted_date: Optional[datetime] = None, + recovery_id: Optional[str] = None, + scheduled_purge_date: Optional[datetime] = None, + **kwargs: Any, + ) -> None: + super(DeletedKey, self).__init__(properties=properties, **kwargs) + self._deleted_date = deleted_date + self._recovery_id = recovery_id + self._scheduled_purge_date = scheduled_purge_date + + def __repr__(self) -> str: + return f""[:1024] + + @classmethod + def _from_deleted_key_bundle(cls, deleted_key_bundle: "_models.DeletedKeyBundle") -> "DeletedKey": + # pylint:disable=protected-access + return cls( + properties=KeyProperties._from_key_bundle(deleted_key_bundle), + key_id=deleted_key_bundle.key.kid, # type: ignore + jwk={field: getattr(deleted_key_bundle.key, field, None) for field in JsonWebKey._FIELDS}, + deleted_date=deleted_key_bundle.deleted_date, + recovery_id=deleted_key_bundle.recovery_id, + scheduled_purge_date=deleted_key_bundle.scheduled_purge_date, + ) + + @classmethod + def _from_deleted_key_item(cls, deleted_key_item: "_models.DeletedKeyItem") -> "DeletedKey": + return cls( + properties=KeyProperties._from_key_item(deleted_key_item), # pylint: disable=protected-access + key_id=deleted_key_item.kid, + deleted_date=deleted_key_item.deleted_date, + recovery_id=deleted_key_item.recovery_id, + scheduled_purge_date=deleted_key_item.scheduled_purge_date, + ) + + @property + def deleted_date(self) -> Optional[datetime]: + """When the key was deleted, in UTC. + + :returns: When the key was deleted, in UTC. + :rtype: ~datetime.datetime or None + """ + return self._deleted_date + + @property + def recovery_id(self) -> Optional[str]: + """An identifier used to recover the deleted key. Returns ``None`` if soft-delete is disabled. + + :returns: An identifier used to recover the deleted key. Returns ``None`` if soft-delete is disabled. + :rtype: str or None + """ + return self._recovery_id + + @property + def scheduled_purge_date(self) -> Optional[datetime]: + """When the key is scheduled to be purged, in UTC. Returns ``None`` if soft-delete is disabled. + + :returns: When the key is scheduled to be purged, in UTC. Returns ``None`` if soft-delete is disabled. + :rtype: ~datetime.datetime or None + """ + return self._scheduled_purge_date diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py new file mode 100644 index 000000000000..6677724a8b67 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_sdk_moniker.py @@ -0,0 +1,7 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from ._version import VERSION + +SDK_MONIKER = f"keyvault-keys/{VERSION}" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py new file mode 100644 index 000000000000..cb088e31ad23 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py @@ -0,0 +1,78 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Optional +from urllib import parse + +from .challenge_auth_policy import ChallengeAuthPolicy +from .client_base import KeyVaultClientBase +from .http_challenge import HttpChallenge +from . import http_challenge_cache + +HttpChallengeCache = http_challenge_cache # to avoid aliasing pylint error (C4745) + + +__all__ = [ + "ChallengeAuthPolicy", + "HttpChallenge", + "HttpChallengeCache", + "KeyVaultClientBase", +] + + +class KeyVaultResourceId: + """Represents a Key Vault identifier and its parsed contents. + + :param str source_id: The complete identifier received from Key Vault + :param str vault_url: The vault URL + :param str name: The name extracted from the ID + :param str version: The version extracted from the ID + """ + + def __init__( + self, + source_id: str, + vault_url: str, + name: str, + version: "Optional[str]" = None, + ) -> None: + self.source_id = source_id + self.vault_url = vault_url + self.name = name + self.version = version + + +def parse_key_vault_id(source_id: str) -> KeyVaultResourceId: + try: + parsed_uri = parse.urlparse(source_id) + except Exception as exc: + raise ValueError(f"'{source_id}' is not a valid ID") from exc + if not (parsed_uri.scheme and parsed_uri.hostname): + raise ValueError(f"'{source_id}' is not a valid ID") + + path = list(filter(None, parsed_uri.path.split("/"))) + + if len(path) < 2 or len(path) > 3: + raise ValueError(f"'{source_id}' is not a valid ID") + + vault_url = f"{parsed_uri.scheme}://{parsed_uri.hostname}" + if parsed_uri.port: + vault_url += f":{parsed_uri.port}" + + return KeyVaultResourceId( + source_id=source_id, + vault_url=vault_url, + name=path[1], + version=path[2] if len(path) == 3 else None, + ) + + +try: + # pylint:disable=unused-import + from .async_challenge_auth_policy import AsyncChallengeAuthPolicy + from .async_client_base import AsyncKeyVaultClientBase + + __all__.extend(["AsyncChallengeAuthPolicy", "AsyncKeyVaultClientBase"]) +except (SyntaxError, ImportError): + pass diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py new file mode 100644 index 000000000000..ff0c398bba6d --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling.py @@ -0,0 +1,136 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import threading +import uuid +from typing import Any, Callable, cast, Optional + +from azure.core.exceptions import ResourceNotFoundError, HttpResponseError +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import HttpTransport +from azure.core.polling import PollingMethod, LROPoller, NoPolling + +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.common import with_current_context + + +class KeyVaultOperationPoller(LROPoller): + """Poller for long running operations where calling result() doesn't wait for operation to complete. + + :param polling_method: The poller's polling method. + :type polling_method: ~azure.core.polling.PollingMethod + """ + + def __init__(self, polling_method: PollingMethod) -> None: + super(KeyVaultOperationPoller, self).__init__(None, None, lambda *_: None, NoPolling()) + self._polling_method = polling_method + + # pylint: disable=arguments-differ + def result(self) -> "Any": # type: ignore + """Returns a representation of the final resource without waiting for the operation to complete. + + :returns: The deserialized resource of the long running operation + :rtype: Any + + :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. + """ + return self._polling_method.resource() + + @distributed_trace + def wait(self, timeout: Optional[float] = None) -> None: + """Wait on the long running operation for a number of seconds. + + You can check if this call has ended with timeout with the "done()" method. + + :param float timeout: Period of time to wait for the long running operation to complete (in seconds). + + :raises ~azure.core.exceptions.HttpResponseError: Server problem with the query. + """ + + if not self._polling_method.finished(): + self._done = threading.Event() + self._thread = threading.Thread( + target=with_current_context(self._start), name=f"KeyVaultOperationPoller({uuid.uuid4()})" + ) + self._thread.daemon = True + self._thread.start() + + if self._thread is None: + return + self._thread.join(timeout=timeout) + try: + # Let's handle possible None in forgiveness here + raise self._exception # type: ignore + except TypeError: # Was None + pass + + +class DeleteRecoverPollingMethod(PollingMethod): + """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. + + This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault + immediately removes it from its collection. However, the resource will not immediately appear in the deleted + collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, + the resource exists in the deleted collection i.e. its deletion is complete. + + Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted + resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. + + :param pipeline_response: The operation's original pipeline response. + :type pipeline_response: PipelineResponse + :param command: A callable to invoke when polling. + :type command: Callable + :param final_resource: The final resource returned by the polling operation. + :type final_resource: Any + :param bool finished: Whether or not the polling operation is completed. + :param int interval: The polling interval, in seconds. + """ + + def __init__( + self, + pipeline_response: PipelineResponse, + command: Callable, + final_resource: Any, + finished: bool, + interval: int = 2, + ) -> None: + self._pipeline_response = pipeline_response + self._command = command + self._resource = final_resource + self._polling_interval = interval + self._finished = finished + + def _update_status(self) -> None: + try: + self._command() + self._finished = True + except ResourceNotFoundError: + pass + except HttpResponseError as e: + # If we are polling on get_deleted_* and we don't have get permissions, we will get + # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. + if e.status_code == 403: + self._finished = True + else: + raise + + def initialize(self, client: Any, initial_response: Any, deserialization_callback: Callable) -> None: + pass + + def run(self) -> None: + while not self.finished(): + self._update_status() + if not self.finished(): + # We should always ask the client's transport to sleep, instead of sleeping directly + transport: HttpTransport = cast(HttpTransport, self._pipeline_response.context.transport) + transport.sleep(self._polling_interval) + + def finished(self) -> bool: + return self._finished + + def resource(self) -> Any: + return self._resource + + def status(self) -> str: + return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py new file mode 100644 index 000000000000..16168229af08 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/_polling_async.py @@ -0,0 +1,80 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Any, Callable, cast + +from azure.core.exceptions import ResourceNotFoundError, HttpResponseError +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import AsyncHttpTransport +from azure.core.polling import AsyncPollingMethod + + +class AsyncDeleteRecoverPollingMethod(AsyncPollingMethod): + """Poller for deleting resources, and recovering deleted resources, in vaults with soft-delete enabled. + + This works by polling for the existence of the deleted or recovered resource. When a resource is deleted, Key Vault + immediately removes it from its collection. However, the resource will not immediately appear in the deleted + collection. Key Vault will therefore respond 404 to GET requests for the deleted resource; when it responds 2xx, + the resource exists in the deleted collection i.e. its deletion is complete. + + Similarly, while recovering a deleted resource, Key Vault will respond 404 to GET requests for the non-deleted + resource; when it responds 2xx, the resource exists in the non-deleted collection, i.e. its recovery is complete. + + :param pipeline_response: The operation's original pipeline response. + :type pipeline_response: PipelineResponse + :param command: An awaitable to invoke when polling. + :type command: Callable + :param final_resource: The final resource returned by the polling operation. + :type final_resource: Any + :param bool finished: Whether or not the polling operation is completed. + :param int interval: The polling interval, in seconds. + """ + + def __init__( + self, + pipeline_response: PipelineResponse, + command: Callable, + final_resource: Any, + finished: bool, + interval: int = 2, + ) -> None: + self._pipeline_response = pipeline_response + self._command = command + self._resource = final_resource + self._polling_interval = interval + self._finished = finished + + def initialize(self, client, initial_response, deserialization_callback): + pass + + async def _update_status(self) -> None: + try: + await self._command() + self._finished = True + except ResourceNotFoundError: + pass + except HttpResponseError as e: + # If we are polling on get_deleted_* and we don't have get permissions, we will get + # ResourceNotFoundError until the resource is recovered, at which point we'll get a 403. + if e.status_code == 403: + self._finished = True + else: + raise + + async def run(self) -> None: + while not self.finished(): + await self._update_status() + if not self.finished(): + # We should always ask the client's transport to sleep, instead of sleeping directly + transport: AsyncHttpTransport = cast(AsyncHttpTransport, self._pipeline_response.context.transport) + await transport.sleep(self._polling_interval) + + def finished(self) -> bool: + return self._finished + + def resource(self) -> Any: + return self._resource + + def status(self) -> str: + return "finished" if self._finished else "polling" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py new file mode 100644 index 000000000000..3e3ac1855178 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py @@ -0,0 +1,256 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Policy implementing Key Vault's challenge authentication protocol. + +Normally the protocol is only used for the client's first service request, upon which: +1. The challenge authentication policy sends a copy of the request, without authorization or content. +2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. +3. The policy authenticates according to the challenge and sends the original request with authorization. + +The policy caches the challenge and thus knows how to authenticate future requests. However, authentication +requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the +protocol again. +""" + +from copy import deepcopy +import sys +import time +from typing import Any, Callable, cast, Optional, overload, TypeVar, Union +from urllib.parse import urlparse + +from typing_extensions import ParamSpec + +from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions +from azure.core.credentials_async import AsyncSupportsTokenInfo, AsyncTokenCredential, AsyncTokenProvider +from azure.core.pipeline import PipelineRequest, PipelineResponse +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy +from azure.core.rest import AsyncHttpResponse, HttpRequest + +from .http_challenge import HttpChallenge +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _has_claims, _update_challenge + +if sys.version_info < (3, 9): + from typing import Awaitable +else: + from collections.abc import Awaitable + + +P = ParamSpec("P") +T = TypeVar("T") + + +@overload +async def await_result(func: Callable[P, Awaitable[T]], *args: P.args, **kwargs: P.kwargs) -> T: ... + + +@overload +async def await_result(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ... + + +async def await_result(func: Callable[P, Union[T, Awaitable[T]]], *args: P.args, **kwargs: P.kwargs) -> T: + """If func returns an awaitable, await it. + + :param func: The function to run. + :type func: callable + :param args: The positional arguments to pass to the function. + :type args: list + :rtype: any + :return: The result of the function + """ + result = func(*args, **kwargs) + if isinstance(result, Awaitable): + return await result + return result + + +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): + """Policy for handling HTTP authentication challenges. + + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity.aio` + :type credential: ~azure.core.credentials_async.AsyncTokenProvider + """ + + def __init__(self, credential: AsyncTokenProvider, *scopes: str, **kwargs: Any) -> None: + # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request + super().__init__(credential, *scopes, enable_cae=True, **kwargs) + self._credential: AsyncTokenProvider = credential + self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None + self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) + self._request_copy: Optional[HttpRequest] = None + + async def send(self, request: PipelineRequest[HttpRequest]) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: + """Authorize request with a bearer token and send it to the next policy. + + We implement this method to account for the valid scenario where a Key Vault authentication challenge is + immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to + the caller, but we should handle that second challenge as well (and only return any third 401 response). + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + await await_result(self.on_request, request) + response: PipelineResponse[HttpRequest, AsyncHttpResponse] + try: + response = await self.next.send(request) + except Exception: # pylint:disable=broad-except + await await_result(self.on_exception, request) + raise + await await_result(self.on_response, request, response) + + if response.http_response.status_code == 401: + return await self.handle_challenge_flow(request, response) + return response + + async def handle_challenge_flow( + self, + request: PipelineRequest[HttpRequest], + response: PipelineResponse[HttpRequest, AsyncHttpResponse], + consecutive_challenge: bool = False, + ) -> PipelineResponse[HttpRequest, AsyncHttpResponse]: + """Handle the challenge flow of Key Vault and CAE authentication. + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + :param response: The pipeline response object + :type response: ~azure.core.pipeline.PipelineResponse + :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. + Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. + True if the preceding challenge was a Key Vault challenge; False otherwise. + + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + self._token = None # any cached token is invalid + if "WWW-Authenticate" in response.http_response.headers: + # If the previous challenge was a KV challenge and this one is too, return the 401 + claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) + if consecutive_challenge and not claims_challenge: + return response + + request_authorized = await self.on_challenge(request, response) + if request_authorized: + # if we receive a challenge response, we retrieve a new token + # which matches the new target. In this case, we don't want to remove + # token from the request so clear the 'insecure_domain_change' tag + request.context.options.pop("insecure_domain_change", False) + try: + response = await self.next.send(request) + except Exception: # pylint:disable=broad-except + await await_result(self.on_exception, request) + raise + + # If consecutive_challenge == True, this could be a third consecutive 401 + if response.http_response.status_code == 401 and not consecutive_challenge: + # If the previous challenge wasn't from CAE, we can try this function one more time + if not claims_challenge: + return await self.handle_challenge_flow(request, response, consecutive_challenge=True) + await await_result(self.on_response, request, response) + return response + + async def on_request(self, request: PipelineRequest) -> None: + _enforce_tls(request) + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token(): + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + await self._request_kv_token(scope, challenge) + + bearer_token = cast(Union[AccessToken, AccessTokenInfo], self._token).token + request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, use the original request including the body, authorize the + # request, and tell super to send it again. + if request.http_request.content: + self._request_copy = request.http_request + bodiless_request = HttpRequest( + method=request.http_request.method, + url=request.http_request.url, + headers=deepcopy(request.http_request.headers), + ) + bodiless_request.headers["Content-Length"] = "0" + request.http_request = bodiless_request + + async def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: + try: + # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary + old_scope: Optional[str] = None + old_tenant: Optional[str] = None + cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if cached_challenge: + old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" + old_tenant = cached_challenge.tenant_id + + challenge = _update_challenge(request, response) + # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary + if challenge.claims and old_scope: + challenge._parameters["scope"] = old_scope # pylint:disable=protected-access + challenge.tenant_id = old_tenant + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + except ValueError: + return False + + if self._verify_challenge_resource: + resource_domain = urlparse(scope).netloc + if not resource_domain: + raise ValueError(f"The challenge contains invalid scope '{scope}'.") + + request_domain = urlparse(request.http_request.url).netloc + if not request_domain.lower().endswith(f".{resource_domain.lower()}"): + raise ValueError( + f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " + "`verify_challenge_resource=False` to your client's constructor to disable this verification. " + "See https://aka.ms/azsdk/blog/vault-uri for more information." + ) + + # If we had created a request copy in on_request, use it now to send along the original body content + if self._request_copy: + request.http_request = self._request_copy + + # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication + # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 + if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): + await self.authorize_request(request, scope, claims=challenge.claims) + else: + await self.authorize_request(request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id) + + return True + + def _need_new_token(self) -> bool: + now = time.time() + refresh_on = getattr(self._token, "refresh_on", None) + return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 + + async def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: + """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. + + :param str scope: The scope for which to request a token. + :param challenge: The challenge for the request being made. + :type challenge: HttpChallenge + """ + # Exclude tenant for AD FS authentication + exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") + # The AsyncSupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs + if hasattr(self._credential, "get_token_info"): + options: TokenRequestOptions = {"enable_cae": True} + if challenge.tenant_id and not exclude_tenant: + options["tenant_id"] = challenge.tenant_id + self._token = await cast(AsyncSupportsTokenInfo, self._credential).get_token_info(scope, options=options) + else: + if exclude_tenant: + self._token = await self._credential.get_token(scope, enable_cae=True) + else: + self._token = await cast(AsyncTokenCredential, self._credential).get_token( + scope, tenant_id=challenge.tenant_id, enable_cae=True + ) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py new file mode 100644 index 000000000000..3e1a2bec8fc4 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_client_base.py @@ -0,0 +1,117 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import sys +from typing import Any + +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.pipeline.policies import HttpLoggingPolicy +from azure.core.rest import AsyncHttpResponse, HttpRequest +from azure.core.tracing.decorator_async import distributed_trace_async + +from . import AsyncChallengeAuthPolicy +from .client_base import ApiVersion, DEFAULT_VERSION, _format_api_version, _SERIALIZER +from .._sdk_moniker import SDK_MONIKER +from .._generated.aio import KeyVaultClient as _KeyVaultClient +from .._generated import models as _models + +if sys.version_info < (3, 9): + from typing import Awaitable +else: + from collections.abc import Awaitable + + +class AsyncKeyVaultClientBase(object): + # pylint:disable=protected-access + def __init__(self, vault_url: str, credential: AsyncTokenCredential, **kwargs: Any) -> None: + if not credential: + raise ValueError( + "credential should be an object supporting the AsyncTokenCredential protocol, " + "such as a credential from azure-identity" + ) + if not vault_url: + raise ValueError("vault_url must be the URL of an Azure Key Vault") + + try: + self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) + # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility + if hasattr(self.api_version, "value"): + self.api_version = self.api_version.value + self._vault_url = vault_url.strip(" /") + + client = kwargs.get("generated_client") + if client: + # caller provided a configured client -> only models left to initialize + self._client = client + models = kwargs.get("generated_models") + self._models = models or _models + return + + http_logging_policy = HttpLoggingPolicy(**kwargs) + http_logging_policy.allowed_header_names.update( + {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} + ) + + verify_challenge = kwargs.pop("verify_challenge_resource", True) + self._client = _KeyVaultClient( + credential=credential, + vault_base_url=self._vault_url, + api_version=self.api_version, + authentication_policy=AsyncChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), + sdk_moniker=SDK_MONIKER, + http_logging_policy=http_logging_policy, + **kwargs, + ) + self._models = _models + except ValueError as exc: + # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum + raise NotImplementedError( + f"This package doesn't support API version '{self.api_version}'. " + + "Supported versions: " + + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] + ) from exc + + @property + def vault_url(self) -> str: + return self._vault_url + + async def __aenter__(self) -> "AsyncKeyVaultClientBase": + await self._client.__aenter__() + return self + + async def __aexit__(self, *args: Any) -> None: + await self._client.__aexit__(*args) + + async def close(self) -> None: + """Close sockets opened by the client. + + Calling this method is unnecessary when using the client as a context manager. + """ + await self._client.close() + + @distributed_trace_async + def send_request( + self, request: HttpRequest, *, stream: bool = False, **kwargs: Any + ) -> Awaitable[AsyncHttpResponse]: + """Runs a network request using the client's existing pipeline. + + The request URL can be relative to the vault URL. The service API version used for the request is the same as + the client's unless otherwise specified. This method does not raise if the response is an error; to raise an + exception, call `raise_for_status()` on the returned response object. For more information about how to send + custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. + + :param request: The network request you want to make. + :type request: ~azure.core.rest.HttpRequest + + :keyword bool stream: Whether the response payload will be streamed. Defaults to False. + + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.rest.AsyncHttpResponse + """ + request_copy = _format_api_version(request, self.api_version) + path_format_arguments = { + "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), + } + request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) + return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py new file mode 100644 index 000000000000..eb4073d0e699 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py @@ -0,0 +1,270 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Policy implementing Key Vault's challenge authentication protocol. + +Normally the protocol is only used for the client's first service request, upon which: +1. The challenge authentication policy sends a copy of the request, without authorization or content. +2. Key Vault responds 401 with a header (the 'challenge') detailing how the client should authenticate such a request. +3. The policy authenticates according to the challenge and sends the original request with authorization. + +The policy caches the challenge and thus knows how to authenticate future requests. However, authentication +requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the +protocol again. +""" + +from copy import deepcopy +import time +from typing import Any, cast, Optional, Union +from urllib.parse import urlparse + +from azure.core.credentials import ( + AccessToken, + AccessTokenInfo, + TokenCredential, + TokenProvider, + TokenRequestOptions, + SupportsTokenInfo, +) +from azure.core.exceptions import ServiceRequestError +from azure.core.pipeline import PipelineRequest, PipelineResponse +from azure.core.pipeline.policies import BearerTokenCredentialPolicy +from azure.core.rest import HttpRequest, HttpResponse + +from .http_challenge import HttpChallenge +from . import http_challenge_cache as ChallengeCache + + +def _enforce_tls(request: PipelineRequest) -> None: + if not request.http_request.url.lower().startswith("https"): + raise ServiceRequestError( + "Bearer token authentication is not permitted for non-TLS protected (non-https) URLs." + ) + + +def _has_claims(challenge: str) -> bool: + """Check if a challenge header contains claims. + + :param challenge: The challenge header to check. + :type challenge: str + + :returns: True if the challenge contains claims; False otherwise. + :rtype: bool + """ + # Split the challenge into its scheme and parameters, then check if any parameter contains claims + split_challenge = challenge.strip().split(" ", 1) + return any("claims=" in item for item in split_challenge[1].split(",")) + + +def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) -> HttpChallenge: + """Parse challenge from a challenge response, cache it, and return it. + + :param request: The pipeline request that prompted the challenge response. + :type request: ~azure.core.pipeline.PipelineRequest + :param challenger: The pipeline response containing the authentication challenge. + :type challenger: ~azure.core.pipeline.PipelineResponse + + :returns: An HttpChallenge object representing the authentication challenge. + :rtype: HttpChallenge + """ + + challenge = HttpChallenge( + request.http_request.url, + challenger.http_response.headers.get("WWW-Authenticate"), + response_headers=challenger.http_response.headers, + ) + ChallengeCache.set_challenge_for_url(request.http_request.url, challenge) + return challenge + + +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): + """Policy for handling HTTP authentication challenges. + + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity` + :type credential: ~azure.core.credentials.TokenProvider + :param str scopes: Lets you specify the type of access needed. + """ + + def __init__(self, credential: TokenProvider, *scopes: str, **kwargs: Any) -> None: + # Pass `enable_cae` so `enable_cae=True` is always passed through self.authorize_request + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, enable_cae=True, **kwargs) + self._credential: TokenProvider = credential + self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None + self._verify_challenge_resource = kwargs.pop("verify_challenge_resource", True) + self._request_copy: Optional[HttpRequest] = None + + def send(self, request: PipelineRequest[HttpRequest]) -> PipelineResponse[HttpRequest, HttpResponse]: + """Authorize request with a bearer token and send it to the next policy. + + We implement this method to account for the valid scenario where a Key Vault authentication challenge is + immediately followed by a CAE claims challenge. The base class's implementation would return the second 401 to + the caller, but we should handle that second challenge as well (and only return any third 401 response). + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + self.on_request(request) + try: + response = self.next.send(request) + except Exception: # pylint:disable=broad-except + self.on_exception(request) + raise + + self.on_response(request, response) + if response.http_response.status_code == 401: + return self.handle_challenge_flow(request, response) + return response + + def handle_challenge_flow( + self, + request: PipelineRequest[HttpRequest], + response: PipelineResponse[HttpRequest, HttpResponse], + consecutive_challenge: bool = False, + ) -> PipelineResponse[HttpRequest, HttpResponse]: + """Handle the challenge flow of Key Vault and CAE authentication. + + :param request: The pipeline request object + :type request: ~azure.core.pipeline.PipelineRequest + :param response: The pipeline response object + :type response: ~azure.core.pipeline.PipelineResponse + :param bool consecutive_challenge: Whether the challenge is arriving immediately after another challenge. + Consecutive challenges can only be valid if a Key Vault challenge is followed by a CAE claims challenge. + True if the preceding challenge was a Key Vault challenge; False otherwise. + + :return: The pipeline response object + :rtype: ~azure.core.pipeline.PipelineResponse + """ + self._token = None # any cached token is invalid + if "WWW-Authenticate" in response.http_response.headers: + # If the previous challenge was a KV challenge and this one is too, return the 401 + claims_challenge = _has_claims(response.http_response.headers["WWW-Authenticate"]) + if consecutive_challenge and not claims_challenge: + return response + + request_authorized = self.on_challenge(request, response) + if request_authorized: + # if we receive a challenge response, we retrieve a new token + # which matches the new target. In this case, we don't want to remove + # token from the request so clear the 'insecure_domain_change' tag + request.context.options.pop("insecure_domain_change", False) + try: + response = self.next.send(request) + except Exception: # pylint:disable=broad-except + self.on_exception(request) + raise + + # If consecutive_challenge == True, this could be a third consecutive 401 + if response.http_response.status_code == 401 and not consecutive_challenge: + # If the previous challenge wasn't from CAE, we can try this function one more time + if not claims_challenge: + return self.handle_challenge_flow(request, response, consecutive_challenge=True) + self.on_response(request, response) + return response + + def on_request(self, request: PipelineRequest) -> None: + _enforce_tls(request) + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._request_kv_token(scope, challenge) + + bearer_token = cast(Union["AccessToken", "AccessTokenInfo"], self._token).token + request.http_request.headers["Authorization"] = f"Bearer {bearer_token}" + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, use the original request including the body, authorize the + # request, and tell super to send it again. + if request.http_request.content: + self._request_copy = request.http_request + bodiless_request = HttpRequest( + method=request.http_request.method, + url=request.http_request.url, + headers=deepcopy(request.http_request.headers), + ) + bodiless_request.headers["Content-Length"] = "0" + request.http_request = bodiless_request + + def on_challenge(self, request: PipelineRequest, response: PipelineResponse) -> bool: + try: + # CAE challenges may not include a scope or tenant; cache from the previous challenge to use if necessary + old_scope: Optional[str] = None + old_tenant: Optional[str] = None + cached_challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if cached_challenge: + old_scope = cached_challenge.get_scope() or cached_challenge.get_resource() + "/.default" + old_tenant = cached_challenge.tenant_id + + challenge = _update_challenge(request, response) + # CAE challenges may not include a scope or tenant; use the previous challenge's values if necessary + if challenge.claims and old_scope: + challenge._parameters["scope"] = old_scope # pylint:disable=protected-access + challenge.tenant_id = old_tenant + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + except ValueError: + return False + + if self._verify_challenge_resource: + resource_domain = urlparse(scope).netloc + if not resource_domain: + raise ValueError(f"The challenge contains invalid scope '{scope}'.") + + request_domain = urlparse(request.http_request.url).netloc + if not request_domain.lower().endswith(f".{resource_domain.lower()}"): + raise ValueError( + f"The challenge resource '{resource_domain}' does not match the requested domain. Pass " + "`verify_challenge_resource=False` to your client's constructor to disable this verification. " + "See https://aka.ms/azsdk/blog/vault-uri for more information." + ) + + # If we had created a request copy in on_request, use it now to send along the original body content + if self._request_copy: + request.http_request = self._request_copy + + # The tenant parsed from AD FS challenges is "adfs"; we don't actually need a tenant for AD FS authentication + # For AD FS we skip cross-tenant authentication per https://github.com/Azure/azure-sdk-for-python/issues/28648 + if challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs"): + self.authorize_request(request, scope, claims=challenge.claims) + else: + self.authorize_request(request, scope, claims=challenge.claims, tenant_id=challenge.tenant_id) + + return True + + @property + def _need_new_token(self) -> bool: + now = time.time() + refresh_on = getattr(self._token, "refresh_on", None) + return not self._token or (refresh_on and refresh_on <= now) or self._token.expires_on - now < 300 + + def _request_kv_token(self, scope: str, challenge: HttpChallenge) -> None: + """Implementation of BearerTokenCredentialPolicy's _request_token method, but specific to Key Vault. + + :param str scope: The scope for which to request a token. + :param challenge: The challenge for the request being made. + :type challenge: HttpChallenge + """ + # Exclude tenant for AD FS authentication + exclude_tenant = challenge.tenant_id and challenge.tenant_id.lower().endswith("adfs") + # The SupportsTokenInfo protocol needs TokenRequestOptions for token requests instead of kwargs + if hasattr(self._credential, "get_token_info"): + options: TokenRequestOptions = {"enable_cae": True} + if challenge.tenant_id and not exclude_tenant: + options["tenant_id"] = challenge.tenant_id + self._token = cast(SupportsTokenInfo, self._credential).get_token_info(scope, options=options) + else: + if exclude_tenant: + self._token = self._credential.get_token(scope, enable_cae=True) + else: + self._token = cast(TokenCredential, self._credential).get_token( + scope, tenant_id=challenge.tenant_id, enable_cae=True + ) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py new file mode 100644 index 000000000000..9e721ef6cfe2 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py @@ -0,0 +1,162 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from copy import deepcopy +from enum import Enum +from typing import Any +from urllib.parse import urlparse + +from azure.core import CaseInsensitiveEnumMeta +from azure.core.credentials import TokenCredential +from azure.core.pipeline.policies import HttpLoggingPolicy +from azure.core.rest import HttpRequest, HttpResponse +from azure.core.tracing.decorator import distributed_trace + +from . import ChallengeAuthPolicy +from .._generated import KeyVaultClient as _KeyVaultClient +from .._generated import models as _models +from .._generated._utils.serialization import Serializer +from .._sdk_moniker import SDK_MONIKER + + +class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Key Vault API versions supported by this package""" + + #: this is the default version + V2025_07_01 = "2025-07-01" + V7_6 = "7.6" + V7_5 = "7.5" + V7_4 = "7.4" + V7_3 = "7.3" + V7_2 = "7.2" + V7_1 = "7.1" + V7_0 = "7.0" + V2016_10_01 = "2016-10-01" + + +DEFAULT_VERSION = ApiVersion.V2025_07_01 + +_SERIALIZER = Serializer() +_SERIALIZER.client_side_validation = False + + +def _format_api_version(request: HttpRequest, api_version: str) -> HttpRequest: + """Returns a request copy that includes an api-version query parameter if one wasn't originally present. + + :param request: The HTTP request being sent. + :type request: ~azure.core.rest.HttpRequest + :param str api_version: The service API version that the request should include. + + :returns: A copy of the request that includes an api-version query parameter. + :rtype: azure.core.rest.HttpRequest + """ + request_copy = deepcopy(request) + params = {"api-version": api_version} # By default, we want to use the client's API version + query = urlparse(request_copy.url).query + + if query: + request_copy.url = request_copy.url.partition("?")[0] + existing_params = {p[0]: p[-1] for p in [p.partition("=") for p in query.split("&")]} + params.update(existing_params) # If an api-version was provided, this will overwrite our default + + # Reconstruct the query parameters onto the URL + query_params = [] + for k, v in params.items(): + query_params.append("{}={}".format(k, v)) + query = "?" + "&".join(query_params) + request_copy.url = request_copy.url + query + return request_copy + + +class KeyVaultClientBase(object): + # pylint:disable=protected-access + def __init__(self, vault_url: str, credential: TokenCredential, **kwargs: Any) -> None: + if not credential: + raise ValueError( + "credential should be an object supporting the TokenCredential protocol, " + "such as a credential from azure-identity" + ) + if not vault_url: + raise ValueError("vault_url must be the URL of an Azure Key Vault") + + try: + self.api_version = kwargs.pop("api_version", DEFAULT_VERSION) + # If API version was provided as an enum value, need to make a plain string for 3.11 compatibility + if hasattr(self.api_version, "value"): + self.api_version = self.api_version.value + self._vault_url = vault_url.strip(" /") + + client = kwargs.get("generated_client") + if client: + # caller provided a configured client -> only models left to initialize + self._client = client + models = kwargs.get("generated_models") + self._models = models or _models + return + + http_logging_policy = HttpLoggingPolicy(**kwargs) + http_logging_policy.allowed_header_names.update( + {"x-ms-keyvault-network-info", "x-ms-keyvault-region", "x-ms-keyvault-service-version"} + ) + + verify_challenge = kwargs.pop("verify_challenge_resource", True) + self._client = _KeyVaultClient( + credential=credential, + vault_base_url=self._vault_url, + api_version=self.api_version, + authentication_policy=ChallengeAuthPolicy(credential, verify_challenge_resource=verify_challenge), + sdk_moniker=SDK_MONIKER, + http_logging_policy=http_logging_policy, + **kwargs, + ) + self._models = _models + except ValueError as exc: + # Ignore pyright error that comes from not identifying ApiVersion as an iterable enum + raise NotImplementedError( + f"This package doesn't support API version '{self.api_version}'. " + + "Supported versions: " + + f"{', '.join(v.value for v in ApiVersion)}" # pyright: ignore[reportGeneralTypeIssues] + ) from exc + + @property + def vault_url(self) -> str: + return self._vault_url + + def __enter__(self) -> "KeyVaultClientBase": + self._client.__enter__() + return self + + def __exit__(self, *args: Any) -> None: + self._client.__exit__(*args) + + def close(self) -> None: + """Close sockets opened by the client. + + Calling this method is unnecessary when using the client as a context manager. + """ + self._client.close() + + @distributed_trace + def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse: + """Runs a network request using the client's existing pipeline. + + The request URL can be relative to the vault URL. The service API version used for the request is the same as + the client's unless otherwise specified. This method does not raise if the response is an error; to raise an + exception, call `raise_for_status()` on the returned response object. For more information about how to send + custom requests with this method, see https://aka.ms/azsdk/dpcodegen/python/send_request. + + :param request: The network request you want to make. + :type request: ~azure.core.rest.HttpRequest + + :keyword bool stream: Whether the response payload will be streamed. Defaults to False. + + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.rest.HttpResponse + """ + request_copy = _format_api_version(request, self.api_version) + path_format_arguments = { + "vaultBaseUrl": _SERIALIZER.url("vault_base_url", self._vault_url, "str", skip_quote=True), + } + request_copy.url = self._client._client.format_url(request_copy.url, **path_format_arguments) + return self._client._client.send_request(request_copy, stream=stream, **kwargs) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py new file mode 100644 index 000000000000..8b14b999de78 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py @@ -0,0 +1,186 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import base64 +from typing import Dict, MutableMapping, Optional +from urllib import parse + + +class HttpChallenge(object): + """An object representing the content of a Key Vault authentication challenge. + + :param str request_uri: The URI of the HTTP request that prompted this challenge. + :param str challenge: The WWW-Authenticate header of the challenge response. + :param response_headers: Optional. The headers attached to the challenge response. + :type response_headers: MutableMapping[str, str] or None + """ + + def __init__( + self, request_uri: str, challenge: str, response_headers: "Optional[MutableMapping[str, str]]" = None + ) -> None: + """Parses an HTTP WWW-Authentication Bearer challenge from a server. + + Example challenge with claims: + Bearer authorization="https://login.windows-ppe.net/", error="invalid_token", + error_description="User session has been revoked", + claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0=" + """ + self.source_authority = self._validate_request_uri(request_uri) + self.source_uri = request_uri + self._parameters: "Dict[str, str]" = {} + + # get the scheme of the challenge and remove from the challenge string + trimmed_challenge = self._validate_challenge(challenge) + split_challenge = trimmed_challenge.split(" ", 1) + self.scheme = split_challenge[0] + trimmed_challenge = split_challenge[1] + + self.claims = None + # split trimmed challenge into comma-separated name=value pairs. Values are expected + # to be surrounded by quotes which are stripped here. + for item in trimmed_challenge.split(","): + # Special case for claims, which can contain = symbols as padding. Assume at most one claim per challenge + if "claims=" in item: + encoded_claims = item[item.index("=") + 1 :].strip(" \"'") + padding_needed = -len(encoded_claims) % 4 + try: + decoded_claims = base64.urlsafe_b64decode(encoded_claims + "=" * padding_needed).decode() + self.claims = decoded_claims + except Exception: # pylint:disable=broad-except + continue + # process name=value pairs + else: + comps = item.split("=") + if len(comps) == 2: + key = comps[0].strip(' "') + value = comps[1].strip(' "') + if key: + self._parameters[key] = value + + # minimum set of parameters + if not self._parameters: + raise ValueError("Invalid challenge parameters") + + # must specify authorization or authorization_uri + if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: + raise ValueError("Invalid challenge parameters") + + authorization_uri = self.get_authorization_server() + # the authorization server URI should look something like https://login.windows.net/tenant-id + raw_uri_path = str(parse.urlparse(authorization_uri).path) + uri_path = raw_uri_path.lstrip("/") + self.tenant_id = uri_path.split("/", maxsplit=1)[0] or None + + # if the response headers were supplied + if response_headers: + # get the message signing key and message key encryption key from the headers + self.server_signature_key = response_headers.get("x-ms-message-signing-key", None) + self.server_encryption_key = response_headers.get("x-ms-message-encryption-key", None) + + def is_bearer_challenge(self) -> bool: + """Tests whether the HttpChallenge is a Bearer challenge. + + :returns: True if the challenge is a Bearer challenge; False otherwise. + :rtype: bool + """ + if not self.scheme: + return False + + return self.scheme.lower() == "bearer" + + def is_pop_challenge(self) -> bool: + """Tests whether the HttpChallenge is a proof of possession challenge. + + :returns: True if the challenge is a proof of possession challenge; False otherwise. + :rtype: bool + """ + if not self.scheme: + return False + + return self.scheme.lower() == "pop" + + def get_value(self, key: str) -> "Optional[str]": + return self._parameters.get(key) + + def get_authorization_server(self) -> str: + """Returns the URI for the authorization server if present, otherwise an empty string. + + :returns: The URI for the authorization server if present, otherwise an empty string. + :rtype: str + """ + value = "" + for key in ["authorization_uri", "authorization"]: + value = self.get_value(key) or "" + if value: + break + return value + + def get_resource(self) -> str: + """Returns the resource if present, otherwise an empty string. + + :returns: The challenge resource if present, otherwise an empty string. + :rtype: str + """ + return self.get_value("resource") or "" + + def get_scope(self) -> str: + """Returns the scope if present, otherwise an empty string. + + :returns: The challenge scope if present, otherwise an empty string. + :rtype: str + """ + return self.get_value("scope") or "" + + def supports_pop(self) -> bool: + """Returns True if the challenge supports proof of possession token auth; False otherwise. + + :returns: True if the challenge supports proof of possession token auth; False otherwise. + :rtype: bool + """ + return self._parameters.get("supportspop", "").lower() == "true" + + def supports_message_protection(self) -> bool: + """Returns True if the challenge vault supports message protection; False otherwise. + + :returns: True if the challenge vault supports message protection; False otherwise. + :rtype: bool + """ + return self.supports_pop() and self.server_encryption_key and self.server_signature_key # type: ignore + + def _validate_challenge( + self, challenge: str + ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use + """Verifies that the challenge is a valid auth challenge and returns the key=value pairs. + + :param str challenge: The WWW-Authenticate header of the challenge response. + + :returns: The challenge key/value pairs, with whitespace removed, as a string. + :rtype: str + """ + if not challenge: + raise ValueError("Challenge cannot be empty") + + return challenge.strip() + + def _validate_request_uri( + self, uri: str + ) -> str: # pylint:disable=bad-option-value,useless-option-value,no-self-use + """Extracts the host authority from the given URI. + + :param str uri: The URI of the HTTP request that prompted the challenge. + + :returns: The challenge host authority. + :rtype: str + """ + if not uri: + raise ValueError("request_uri cannot be empty") + + parsed = parse.urlparse(uri) + if not parsed.netloc: + raise ValueError("request_uri must be an absolute URI") + + if parsed.scheme.lower() not in ["http", "https"]: + raise ValueError("request_uri must be HTTP or HTTPS") + + return parsed.netloc diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py new file mode 100644 index 000000000000..99f32091e24b --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py @@ -0,0 +1,93 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import threading +from typing import Dict, Optional +from urllib import parse + +from .http_challenge import HttpChallenge + + +_cache: "Dict[str, HttpChallenge]" = {} +_lock = threading.Lock() + + +def get_challenge_for_url(url: str) -> "Optional[HttpChallenge]": + """Gets the challenge for the cached URL. + + :param str url: the URL the challenge is cached for. + + :returns: The challenge for the cached request URL, or None if the request URL isn't cached. + :rtype: HttpChallenge or None + """ + + if not url: + raise ValueError("URL cannot be None") + + key = _get_cache_key(url) + + with _lock: + return _cache.get(key.lower()) + + +def _get_cache_key(url: str) -> str: + """Use the URL's netloc as cache key except when the URL specifies the default port for its scheme. In that case + use the netloc without the port. That is to say, https://foo.bar and https://foo.bar:443 are considered equivalent. + + This equivalency prevents an unnecessary challenge when using Key Vault's paging API. The Key Vault client doesn't + specify ports, but Key Vault's next page links do, so a redundant challenge would otherwise be executed when the + client requests the next page. + + :param str url: The HTTP request URL. + + :returns: The URL's `netloc`, minus any port attached to the URL. + :rtype: str + """ + + parsed = parse.urlparse(url) + if parsed.scheme == "https" and parsed.port == 443: + return parsed.netloc[:-4] + return parsed.netloc + + +def remove_challenge_for_url(url: str) -> None: + """Removes the cached challenge for the specified URL. + + :param str url: the URL for which to remove the cached challenge + """ + if not url: + raise ValueError("URL cannot be empty") + + key = _get_cache_key(url) + with _lock: + del _cache[key.lower()] + + +def set_challenge_for_url(url: str, challenge: "HttpChallenge") -> None: + """Caches the challenge for the specified URL. + + :param str url: the URL for which to cache the challenge + :param challenge: the challenge to cache + :type challenge: HttpChallenge + """ + if not url: + raise ValueError("URL cannot be empty") + + if not challenge: + raise ValueError("Challenge cannot be empty") + + src_url = parse.urlparse(url) + if src_url.netloc.lower() != challenge.source_authority.lower(): + raise ValueError("Source URL and Challenge URL do not match") + + key = _get_cache_key(url) + with _lock: + _cache[key.lower()] = challenge + + +def clear() -> None: + """Clears the cache.""" + + with _lock: + _cache.clear() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py new file mode 100644 index 000000000000..de39d939367f --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_version.py @@ -0,0 +1,6 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +VERSION = "4.11.1" diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py new file mode 100644 index 000000000000..71cad7e66b18 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/__init__.py @@ -0,0 +1,7 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from ._client import KeyClient + +__all__ = ["KeyClient"] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py new file mode 100644 index 000000000000..41084614a416 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/aio/_client.py @@ -0,0 +1,1017 @@ +# pylint: disable=too-many-lines +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +# pylint:disable=too-many-lines +from datetime import datetime +from functools import partial +from typing import Any, Dict, List, Optional, Union + +from azure.core.async_paging import AsyncItemPaged +from azure.core.tracing.decorator import distributed_trace +from azure.core.tracing.decorator_async import distributed_trace_async + +from ..crypto.aio import CryptographyClient +from .._client import _get_key_id +from .._enums import KeyCurveName, KeyExportEncryptionAlgorithm, KeyOperation +from .._generated.models import KeyAttributes +from .._shared._polling_async import AsyncDeleteRecoverPollingMethod +from .._shared import AsyncKeyVaultClientBase +from .. import ( + DeletedKey, + JsonWebKey, + KeyProperties, + KeyReleasePolicy, + KeyRotationLifetimeAction, + KeyRotationPolicy, + KeyType, + KeyVaultKey, + ReleaseKeyResult, +) + + +class KeyClient(AsyncKeyVaultClientBase): + """A high-level asynchronous interface for managing a vault's keys. + + :param str vault_url: URL of the vault the client will access. This is also called the vault's "DNS Name". + You should validate that this URL references a valid Key Vault or Managed HSM resource. + See https://aka.ms/azsdk/blog/vault-uri for details. + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity.aio` + :type credential: ~azure.core.credentials_async.AsyncTokenCredential + + :keyword api_version: Version of the service API to use. Defaults to the most recent. + :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str + :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key + Vault or Managed HSM domain. Defaults to True. + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START create_key_client] + :end-before: [END create_key_client] + :language: python + :caption: Create a new ``KeyClient`` + :dedent: 4 + """ + + # pylint:disable=protected-access, too-many-public-methods + + def _get_attributes( + self, + enabled: Optional[bool], + not_before: Optional[datetime], + expires_on: Optional[datetime], + exportable: Optional[bool] = None, + ) -> Optional[KeyAttributes]: + """Return a KeyAttributes object if non-None attributes are provided, or None otherwise. + + :param enabled: Whether the key is enabled. + :type enabled: bool or None + :param not_before: Not before date of the key in UTC. + :type not_before: ~datetime.datetime or None + :param expires_on: Expiry date of the key in UTC. + :type expires_on: ~datetime.datetime or None + :param exportable: Whether the private key can be exported. + :type exportable: bool or None + + :returns: An autorest-generated model of the key's attributes. + :rtype: KeyAttributes + """ + if enabled is not None or not_before is not None or expires_on is not None or exportable is not None: + return self._models.KeyAttributes( + enabled=enabled, not_before=not_before, expires=expires_on, exportable=exportable + ) + return None + + def get_cryptography_client( + self, + key_name: str, + *, + key_version: Optional[str] = None, + **kwargs, # pylint: disable=unused-argument + ) -> CryptographyClient: + """Gets a :class:`~azure.keyvault.keys.crypto.aio.CryptographyClient` for the given key. + + :param str key_name: The name of the key used to perform cryptographic operations. + + :keyword key_version: Optional version of the key used to perform cryptographic operations. + :paramtype key_version: str or None + + :returns: A :class:`~azure.keyvault.keys.crypto.aio.CryptographyClient` using the same options, credentials, and + HTTP client as this :class:`~azure.keyvault.keys.aio.KeyClient`. + :rtype: ~azure.keyvault.keys.crypto.aio.CryptographyClient + """ + key_id = _get_key_id(self._vault_url, key_name, key_version) + + # We provide a fake credential because the generated client already has the KeyClient's real credential + return CryptographyClient( + key_id, object(), generated_client=self._client, generated_models=self._models # type: ignore + ) + + @distributed_trace_async + async def create_key( + self, + name: str, + key_type: Union[str, KeyType], + *, + size: Optional[int] = None, + curve: Optional[Union[str, KeyCurveName]] = None, + public_exponent: Optional[int] = None, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a key or, if ``name`` is already in use, create a new version of the key. + + Requires keys/create permission. + + :param str name: The name of the new key. + :param key_type: The type of key to create + :type key_type: ~azure.keyvault.keys.KeyType or str + + :keyword size: Key size in bits. Applies only to RSA and symmetric keys. Consider using + :func:`create_rsa_key` or :func:`create_oct_key` instead. + :paramtype size: int or None + :keyword curve: Elliptic curve name. Applies only to elliptic curve keys. Defaults to the NIST P-256 + elliptic curve. To create an elliptic curve key, consider using :func:`create_ec_key` instead. + :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None + :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. + :paramtype public_exponent: int or None + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START create_key] + :end-before: [END create_key] + :language: python + :caption: Create a key + :dedent: 8 + """ + attributes = self._get_attributes( + enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable + ) + + policy = release_policy + if policy is not None: + policy = self._models.KeyReleasePolicy( + encoded_policy=policy.encoded_policy, content_type=policy.content_type, immutable=policy.immutable + ) + parameters = self._models.KeyCreateParameters( + kty=key_type, + key_size=size, + key_attributes=attributes, + key_ops=key_operations, + tags=tags, + curve=curve, + public_exponent=public_exponent, + release_policy=policy, + ) + + bundle = await self._client.create_key( + key_name=name, + parameters=parameters, + **kwargs, + ) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace_async + async def create_rsa_key( + self, + name: str, + *, + size: Optional[int] = None, + public_exponent: Optional[int] = None, + hardware_protected: Optional[bool] = False, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a new RSA key or, if ``name`` is already in use, create a new version of the key + + Requires the keys/create permission. + + :param str name: The name for the new key. + + :keyword size: Key size in bits, for example 2048, 3072, or 4096. + :paramtype size: int or None + :keyword public_exponent: The RSA public exponent to use. Applies only to RSA keys created in a Managed HSM. + :paramtype public_exponent: int or None + :keyword hardware_protected: Whether the key should be created in a hardware security module. + Defaults to ``False``. + :paramtype hardware_protected: bool or None + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START create_rsa_key] + :end-before: [END create_rsa_key] + :language: python + :caption: Create RSA key + :dedent: 8 + """ + return await self.create_key( + name, + key_type="RSA-HSM" if hardware_protected else "RSA", + size=size, + public_exponent=public_exponent, + key_operations=key_operations, + enabled=enabled, + tags=tags, + not_before=not_before, + expires_on=expires_on, + exportable=exportable, + release_policy=release_policy, + **kwargs, + ) + + @distributed_trace_async + async def create_ec_key( + self, + name: str, + *, + curve: Optional[Union[str, KeyCurveName]] = None, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + hardware_protected: Optional[bool] = False, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a new elliptic curve key or, if ``name`` is already in use, create a new version of the key. + + Requires the keys/create permission. + + :param str name: The name for the new key. + + :keyword curve: Elliptic curve name. Defaults to the NIST P-256 elliptic curve. + :paramtype curve: ~azure.keyvault.keys.KeyCurveName or str or None + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword hardware_protected: Whether the key should be created in a hardware security module. + Defaults to ``False``. + :paramtype hardware_protected: bool or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START create_ec_key] + :end-before: [END create_ec_key] + :language: python + :caption: Create an elliptic curve key + :dedent: 8 + """ + return await self.create_key( + name, + key_type="EC-HSM" if hardware_protected else "EC", + curve=curve, + key_operations=key_operations, + enabled=enabled, + tags=tags, + not_before=not_before, + expires_on=expires_on, + exportable=exportable, + release_policy=release_policy, + **kwargs, + ) + + @distributed_trace_async + async def create_oct_key( + self, + name: str, + *, + size: Optional[int] = None, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + hardware_protected: Optional[bool] = False, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Create a new octet sequence (symmetric) key or, if ``name`` is in use, create a new version of the key. + + Requires the keys/create permission. + + :param str name: The name for the new key. + + :keyword size: Key size in bits, for example 128, 192, or 256. + :paramtype size: int or None + :keyword key_operations: Allowed key operations. + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword hardware_protected: Whether the key should be created in a hardware security module. + Defaults to ``False``. + :paramtype hardware_protected: bool or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The created key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START create_oct_key] + :end-before: [END create_oct_key] + :language: python + :caption: Create an octet sequence (symmetric) key + :dedent: 8 + """ + return await self.create_key( + name, + key_type="oct-HSM" if hardware_protected else "oct", + size=size, + key_operations=key_operations, + enabled=enabled, + tags=tags, + not_before=not_before, + expires_on=expires_on, + exportable=exportable, + release_policy=release_policy, + **kwargs, + ) + + @distributed_trace_async + async def delete_key(self, name: str, **kwargs: Any) -> DeletedKey: + """Delete all versions of a key and its cryptographic material. + + Requires keys/delete permission. If the vault has soft-delete enabled, deletion may take several seconds to + complete. + + :param str name: The name of the key to delete + + :returns: The deleted key + :rtype: ~azure.keyvault.keys.DeletedKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START delete_key] + :end-before: [END delete_key] + :language: python + :caption: Delete a key + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, deleted_key_bundle = await self._client.delete_key( + key_name=name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + deleted_key = DeletedKey._from_deleted_key_bundle(deleted_key_bundle) + + polling_method = AsyncDeleteRecoverPollingMethod( + # no recovery ID means soft-delete is disabled, in which case we initialize the poller as finished + finished=deleted_key.recovery_id is None, + pipeline_response=pipeline_response, + command=partial(self.get_deleted_key, name=name, **kwargs), + final_resource=deleted_key, + interval=polling_interval, + ) + await polling_method.run() + + return polling_method.resource() + + @distributed_trace_async + async def get_key(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: + """Get a key's attributes and, if it's an asymmetric key, its public material. + + Requires keys/get permission. + + :param str name: The name of the key to get. + :param version: (optional) A specific version of the key to get. If not specified, gets the latest version + of the key. + :type version: str or None + + :returns: The fetched key. + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START get_key] + :end-before: [END get_key] + :language: python + :caption: Get a key + :dedent: 8 + """ + if version is None: + version = "" + + bundle = await self._client.get_key(name, version, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace_async + async def get_deleted_key(self, name: str, **kwargs: Any) -> DeletedKey: + """Get a deleted key. Possible only in a vault with soft-delete enabled. + + Requires keys/get permission. + + :param str name: The name of the key + + :returns: The deleted key + :rtype: ~azure.keyvault.keys.DeletedKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START get_deleted_key] + :end-before: [END get_deleted_key] + :language: python + :caption: Get a deleted key + :dedent: 8 + """ + bundle = await self._client.get_deleted_key(name, **kwargs) + return DeletedKey._from_deleted_key_bundle(bundle) + + @distributed_trace + def list_deleted_keys(self, **kwargs: Any) -> AsyncItemPaged[DeletedKey]: + """List all deleted keys, including the public part of each. Possible only in a vault with soft-delete enabled. + + Requires keys/list permission. + + :returns: An iterator of deleted keys + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.keyvault.keys.DeletedKey] + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START list_deleted_keys] + :end-before: [END list_deleted_keys] + :language: python + :caption: List all the deleted keys + :dedent: 8 + """ + return self._client.get_deleted_keys( + maxresults=kwargs.pop("max_page_size", None), + cls=lambda objs: [DeletedKey._from_deleted_key_item(x) for x in objs], + **kwargs, + ) + + @distributed_trace + def list_properties_of_keys(self, **kwargs: Any) -> AsyncItemPaged[KeyProperties]: + """List identifiers and properties of all keys in the vault. + + Requires keys/list permission. + + :returns: An iterator of keys without their cryptographic material or version information + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.keyvault.keys.KeyProperties] + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START list_keys] + :end-before: [END list_keys] + :language: python + :caption: List all keys + :dedent: 8 + """ + return self._client.get_keys( + maxresults=kwargs.pop("max_page_size", None), + cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], + **kwargs, + ) + + @distributed_trace + def list_properties_of_key_versions(self, name: str, **kwargs: Any) -> AsyncItemPaged[KeyProperties]: + """List the identifiers and properties of a key's versions. + + Requires keys/list permission. + + :param str name: The name of the key + + :returns: An iterator of keys without their cryptographic material + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.keyvault.keys.KeyProperties] + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START list_properties_of_key_versions] + :end-before: [END list_properties_of_key_versions] + :language: python + :caption: List all versions of a key + :dedent: 8 + """ + return self._client.get_key_versions( + name, + maxresults=kwargs.pop("max_page_size", None), + cls=lambda objs: [KeyProperties._from_key_item(x) for x in objs], + **kwargs, + ) + + @distributed_trace_async + async def purge_deleted_key(self, name: str, **kwargs: Any) -> None: + """Permanently deletes a deleted key. Only possible in a vault with soft-delete enabled. + + Performs an irreversible deletion of the specified key, without possibility for recovery. The operation is not + available if the :py:attr:`~azure.keyvault.keys.KeyProperties.recovery_level` does not specify 'Purgeable'. + This method is only necessary for purging a key before its + :py:attr:`~azure.keyvault.keys.DeletedKey.scheduled_purge_date`. + + Requires keys/purge permission. + + :param str name: The name of the deleted key to purge + + :returns: None + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # if the vault has soft-delete enabled, purge permanently deletes a deleted key + # (with soft-delete disabled, delete_key is permanent) + await key_client.purge_deleted_key("key-name") + + """ + await self._client.purge_deleted_key(name, **kwargs) + + @distributed_trace_async + async def recover_deleted_key(self, name: str, **kwargs: Any) -> KeyVaultKey: + """Recover a deleted key to its latest version. Possible only in a vault with soft-delete enabled. + + Requires keys/recover permission. If the vault does not have soft-delete enabled, :func:`delete_key` is + permanent, and this method will raise an error. Attempting to recover a non-deleted key will also raise an + error. + + :param str name: The name of the deleted key + + :returns: The recovered key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START recover_deleted_key] + :end-before: [END recover_deleted_key] + :language: python + :caption: Recover a deleted key + :dedent: 8 + """ + polling_interval = kwargs.pop("_polling_interval", None) + if polling_interval is None: + polling_interval = 2 + pipeline_response, recovered_key_bundle = await self._client.recover_deleted_key( + key_name=name, + cls=lambda pipeline_response, deserialized, _: (pipeline_response, deserialized), + **kwargs, + ) + recovered_key = KeyVaultKey._from_key_bundle(recovered_key_bundle) + + command = partial(self.get_key, name=name, **kwargs) + polling_method = AsyncDeleteRecoverPollingMethod( + pipeline_response=pipeline_response, + command=command, + final_resource=recovered_key, + finished=False, + interval=polling_interval, + ) + await polling_method.run() + + return polling_method.resource() + + @distributed_trace_async + async def update_key_properties( + self, + name: str, + version: Optional[str] = None, + *, + key_operations: Optional[List[Union[str, KeyOperation]]] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Change a key's properties (not its cryptographic material). + + Requires keys/update permission. + + :param str name: The name of key to update + :param version: (optional) The version of the key to update. If unspecified, the latest version is updated. + :type version: str or None + + :keyword key_operations: Allowed key operations + :paramtype key_operations: List[~azure.keyvault.keys.KeyOperation or str] or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The updated key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START update_key] + :end-before: [END update_key] + :language: python + :caption: Update a key's attributes + :dedent: 8 + """ + attributes = self._get_attributes(enabled=enabled, not_before=not_before, expires_on=expires_on) + + policy = release_policy + if policy is not None: + policy = self._models.KeyReleasePolicy( + content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable + ) + parameters = self._models.KeyUpdateParameters( + key_ops=key_operations, + key_attributes=attributes, + tags=tags, + release_policy=policy, + ) + + bundle = await self._client.update_key( + name, + key_version=version or "", + parameters=parameters, + **kwargs, + ) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace_async + async def backup_key(self, name: str, **kwargs: Any) -> bytes: + """Back up a key in a protected form useable only by Azure Key Vault. + + Requires key/backup permission. This is intended to allow copying a key from one vault to another. Both vaults + must be owned by the same Azure subscription. Also, backup / restore cannot be performed across geopolitical + boundaries. For example, a backup from a vault in a USA region cannot be restored to a vault in an EU region. + + :param str name: The name of the key to back up + + :returns: The key backup result, in a protected bytes format that can only be used by Azure Key Vault. + :rtype: bytes + + :raises ~azure.core.exceptions.ResourceNotFoundError or ~azure.core.exceptions.HttpResponseError: + the former if the key doesn't exist; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START backup_key] + :end-before: [END backup_key] + :language: python + :caption: Get a key backup + :dedent: 8 + """ + backup_result = await self._client.backup_key(name, **kwargs) + return backup_result.value + + @distributed_trace_async + async def restore_key_backup(self, backup: bytes, **kwargs: Any) -> KeyVaultKey: + """Restore a key backup to the vault. + + Requires keys/restore permission. This imports all versions of the key, with its name, attributes, and access + control policies. If the key's name is already in use, restoring it will fail. Also, the target vault must be + owned by the same Microsoft Azure subscription as the source vault. + + :param bytes backup: A key backup as returned by :func:`backup_key` + + :returns: The restored key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.ResourceExistsError or ~azure.core.exceptions.HttpResponseError: + the former if the backed up key's name is already in use; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_samples_keys_async.py + :start-after: [START restore_key_backup] + :end-before: [END restore_key_backup] + :language: python + :caption: Restore a key backup + :dedent: 8 + """ + bundle = await self._client.restore_key( + parameters=self._models.KeyRestoreParameters(key_bundle_backup=backup), + **kwargs, + ) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace_async + async def import_key( + self, + name: str, + key: JsonWebKey, + *, + hardware_protected: Optional[bool] = None, + enabled: Optional[bool] = None, + tags: Optional[Dict[str, str]] = None, + not_before: Optional[datetime] = None, + expires_on: Optional[datetime] = None, + exportable: Optional[bool] = None, + release_policy: Optional[KeyReleasePolicy] = None, + **kwargs: Any, + ) -> KeyVaultKey: + """Import a key created externally. + + Requires keys/import permission. If ``name`` is already in use, the key will be imported as a new version. + + :param str name: Name for the imported key + :param key: The JSON web key to import + :type key: ~azure.keyvault.keys.JsonWebKey + + :keyword hardware_protected: Whether the key should be backed by a hardware security module + :paramtype hardware_protected: bool or None + :keyword enabled: Whether the key is enabled for use. + :paramtype enabled: bool or None + :keyword tags: Application specific metadata in the form of key-value pairs. + :paramtype tags: dict[str, str] or None + :keyword not_before: Not before date of the key in UTC + :paramtype not_before: ~datetime.datetime or None + :keyword expires_on: Expiry date of the key in UTC + :paramtype expires_on: ~datetime.datetime or None + :keyword exportable: Whether the private key can be exported. + :paramtype exportable: bool or None + :keyword release_policy: The policy rules under which the key can be exported. + :paramtype release_policy: ~azure.keyvault.keys.KeyReleasePolicy or None + + :returns: The imported key + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + """ + attributes = self._get_attributes( + enabled=enabled, not_before=not_before, expires_on=expires_on, exportable=exportable + ) + + policy = release_policy + if policy is not None: + policy = self._models.KeyReleasePolicy( + content_type=policy.content_type, encoded_policy=policy.encoded_policy, immutable=policy.immutable + ) + parameters = self._models.KeyImportParameters( + key=key._to_generated_model(), + key_attributes=attributes, + hsm=hardware_protected, + tags=tags, + release_policy=policy, + ) + + bundle = await self._client.import_key(name, parameters=parameters, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace_async + async def release_key( + self, + name: str, + target_attestation_token: str, + *, + version: Optional[str] = None, + algorithm: Optional[Union[str, KeyExportEncryptionAlgorithm]] = None, + nonce: Optional[str] = None, + **kwargs: Any, + ) -> ReleaseKeyResult: + """Releases a key. + + The release key operation is applicable to all key types. The target key must be marked + exportable. This operation requires the keys/release permission. + + :param str name: The name of the key to get. + :param str target_attestation_token: The attestation assertion for the target of the key release. + + :keyword version: A specific version of the key to release. If unspecified, the latest version is released. + :paramtype version: str or None + :keyword algorithm: The encryption algorithm to use to protect the released key material. + :paramtype algorithm: str or ~azure.keyvault.keys.KeyExportEncryptionAlgorithm or None + :keyword nonce: A client-provided nonce for freshness. + :paramtype nonce: str or None + + :return: The result of the key release. + :rtype: ~azure.keyvault.keys.ReleaseKeyResult + + :raises ~azure.core.exceptions.HttpResponseError: + """ + result = await self._client.release( + key_name=name, + key_version=version or "", + parameters=self._models.KeyReleaseParameters( + target_attestation_token=target_attestation_token, + nonce=nonce, + enc=algorithm, + ), + **kwargs, + ) + return ReleaseKeyResult(result.value) + + @distributed_trace_async + async def get_random_bytes(self, count: int, **kwargs: Any) -> bytes: + """Get the requested number of random bytes from a managed HSM. + + :param int count: The requested number of random bytes. + + :return: The random bytes. + :rtype: bytes + + :raises ValueError or ~azure.core.exceptions.HttpResponseError: + the former if less than one random byte is requested; the latter for other errors + + Example: + .. literalinclude:: ../tests/test_keys_async.py + :start-after: [START get_random_bytes] + :end-before: [END get_random_bytes] + :language: python + :caption: Get random bytes + :dedent: 12 + """ + if count < 1: + raise ValueError("At least one random byte must be requested") + parameters = self._models.GetRandomBytesRequest(count=count) + result = await self._client.get_random_bytes(parameters=parameters, **kwargs) + return result.value + + @distributed_trace_async + async def get_key_rotation_policy(self, key_name: str, **kwargs: Any) -> KeyRotationPolicy: + """Get the rotation policy of a Key Vault key. + + :param str key_name: The name of the key. + + :return: The key rotation policy. + :rtype: ~azure.keyvault.keys.KeyRotationPolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + policy = await self._client.get_key_rotation_policy(key_name=key_name, **kwargs) + return KeyRotationPolicy._from_generated(policy) + + @distributed_trace_async + async def rotate_key(self, name: str, **kwargs: Any) -> KeyVaultKey: + """Rotate the key based on the key policy by generating a new version of the key. + + This operation requires the keys/rotate permission. + + :param str name: The name of the key to rotate. + + :return: The new version of the rotated key. + :rtype: ~azure.keyvault.keys.KeyVaultKey + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = await self._client.rotate_key(key_name=name, **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + @distributed_trace_async + async def update_key_rotation_policy( # pylint: disable=unused-argument + self, + key_name: str, + policy: KeyRotationPolicy, + *, + lifetime_actions: Optional[List[KeyRotationLifetimeAction]] = None, + expires_in: Optional[str] = None, + **kwargs: Any, + ) -> KeyRotationPolicy: + """Updates the rotation policy of a Key Vault key. + + This operation requires the keys/update permission. + + :param str key_name: The name of the key in the given vault. + :param policy: The new rotation policy for the key. + :type policy: ~azure.keyvault.keys.KeyRotationPolicy + + :keyword lifetime_actions: Actions that will be performed by Key Vault over the lifetime of a key. This will + override the lifetime actions of the provided ``policy``. + :paramtype lifetime_actions: List[~azure.keyvault.keys.KeyRotationLifetimeAction] + :keyword str expires_in: The expiry time of the policy that will be applied on new key versions, defined as an + ISO 8601 duration. For example: 90 days is "P90D", 3 months is "P3M", and 48 hours is "PT48H". See + `Wikipedia `_ for more information on ISO 8601 durations. + This will override the expiry time of the provided ``policy``. + + :return: The updated rotation policy. + :rtype: ~azure.keyvault.keys.KeyRotationPolicy + + :raises ~azure.core.exceptions.HttpResponseError: + """ + actions = lifetime_actions or policy.lifetime_actions + if actions: + actions = [ + self._models.LifetimeActions( + action=self._models.LifetimeActionsType(type=action.action), + trigger=self._models.LifetimeActionsTrigger( + time_after_create=action.time_after_create, time_before_expiry=action.time_before_expiry + ), + ) + for action in actions + ] + + attributes = self._models.KeyRotationPolicyAttributes(expiry_time=expires_in or policy.expires_in) + new_policy = self._models.KeyRotationPolicy(lifetime_actions=actions or [], attributes=attributes) + result = await self._client.update_key_rotation_policy(key_name=key_name, key_rotation_policy=new_policy) + return KeyRotationPolicy._from_generated(result) + + @distributed_trace_async + async def get_key_attestation(self, name: str, version: Optional[str] = None, **kwargs: Any) -> KeyVaultKey: + """Get a key and its attestation blob. + + This method is applicable to any key stored in Azure Key Vault Managed HSM. This operation requires the keys/get + permission. + + :param str name: The name of the key. + :param version: (optional) A specific version of the key to get. If not specified, gets the latest version + of the key. + :type version: str or None + + :return: The key attestation. + :rtype: ~azure.keyvault.keys.KeyAttestation + + :raises ~azure.core.exceptions.HttpResponseError: + """ + bundle = await self._client.get_key_attestation(key_name=name, key_version=version or "", **kwargs) + return KeyVaultKey._from_key_bundle(bundle) + + async def __aenter__(self) -> "KeyClient": + await self._client.__aenter__() + return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py new file mode 100644 index 000000000000..9e931898fc8e --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py @@ -0,0 +1,32 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from ._models import ( + DecryptResult, + EncryptResult, + KeyVaultRSAPrivateKey, + KeyVaultRSAPublicKey, + SignResult, + WrapResult, + VerifyResult, + UnwrapResult, +) +from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm +from ._client import CryptographyClient + + +__all__ = [ + "CryptographyClient", + "DecryptResult", + "EncryptionAlgorithm", + "EncryptResult", + "KeyVaultRSAPrivateKey", + "KeyVaultRSAPublicKey", + "KeyWrapAlgorithm", + "SignatureAlgorithm", + "SignResult", + "WrapResult", + "VerifyResult", + "UnwrapResult", +] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py new file mode 100644 index 000000000000..51f273d8f858 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py @@ -0,0 +1,577 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from datetime import datetime +import logging +from typing import Any, cast, Dict, Optional, Union + +from azure.core.credentials import TokenCredential +from azure.core.exceptions import HttpResponseError +from azure.core.tracing.decorator import distributed_trace + +from . import ( + DecryptResult, + EncryptionAlgorithm, + EncryptResult, + KeyWrapAlgorithm, + SignatureAlgorithm, + SignResult, + VerifyResult, + UnwrapResult, + WrapResult, +) +from ._key_validity import raise_if_time_invalid +from ._models import KeyVaultRSAPrivateKey, KeyVaultRSAPublicKey +from ._providers import get_local_cryptography_provider, NoLocalCryptography +from .. import KeyOperation +from .._models import JsonWebKey, KeyVaultKey +from .._shared import KeyVaultClientBase, KeyVaultResourceId, parse_key_vault_id + +_LOGGER = logging.getLogger(__name__) + + +def _validate_arguments( + operation: KeyOperation, + algorithm: EncryptionAlgorithm, + *, + iv: Optional[bytes] = None, + tag: Optional[bytes] = None, + aad: Optional[bytes] = None, +) -> None: + """Validates the arguments passed to perform an operation with a provided algorithm. + + :param KeyOperation operation: the type of operation being requested + :param EncryptionAlgorithm algorithm: the encryption algorithm to use for the operation + + :keyword iv: initialization vector + :paramtype iv: bytes or None + :keyword tag: authentication tag returned from an encryption + :paramtype tag: bytes or None + :keyword aad: data that is authenticated but not encrypted + :paramtype aad: bytes or None + + :raises ValueError: if parameters that are incompatible with the specified algorithm are provided. + """ + if operation == KeyOperation.encrypt: + if iv and "CBC" not in algorithm: + raise ValueError(f"iv should only be provided with AES-CBC algorithms; {algorithm} does not accept an iv") + if iv is None and "CBC" in algorithm: + raise ValueError("iv is a required parameter for encryption with AES-CBC algorithms.") + if aad and not ("CBC" in algorithm or "GCM" in algorithm): + raise ValueError( + f"additional_authenticated_data should only be provided with AES algorithms; {algorithm} does not " + "accept additional authenticated data" + ) + + if operation == KeyOperation.decrypt: + if iv and not ("CBC" in algorithm or "GCM" in algorithm): + raise ValueError(f"iv should only be provided with AES algorithms; {algorithm} does not accept an iv") + if iv is None and ("CBC" in algorithm or "GCM" in algorithm): + raise ValueError("iv is a required parameter for decryption with AES algorithms.") + if tag and "GCM" not in algorithm: + raise ValueError( + f"authentication_tag should only be provided with AES-GCM algorithms; {algorithm} does not accept a tag" + ) + if tag is None and "GCM" in algorithm: + raise ValueError("authentication_tag is a required parameter for AES-GCM decryption.") + if aad and not ("CBC" in algorithm or "GCM" in algorithm): + raise ValueError( + f"additional_authenticated_data should only be provided with AES algorithms; {algorithm} does not " + "accept additional authenticated data" + ) + + +class CryptographyClient(KeyVaultClientBase): + """Performs cryptographic operations using Azure Key Vault keys. + + This client will perform operations locally when it's intialized with the necessary key material or is able to get + that material from Key Vault. When the required key material is unavailable, cryptographic operations are performed + by the Key Vault service. + + :param key: Either a azure.keyvault.keys.KeyVaultKey instance as returned by + :func:`~azure.keyvault.keys.KeyClient.get_key`, or a string. + If a string, the value must be the identifier of an Azure Key Vault key. Including a version is recommended. + :type key: str or azure.keyvault.keys.KeyVaultKey + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity` + :type credential: ~azure.core.credentials.TokenCredential + + :keyword api_version: Version of the service API to use. Defaults to the most recent. + :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str + :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key + Vault or Managed HSM domain. Defaults to True. + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START create_client] + :end-before: [END create_client] + :caption: Create a CryptographyClient + :language: python + :dedent: 8 + """ + + # pylint:disable=protected-access + + def __init__(self, key: Union[KeyVaultKey, str], credential: TokenCredential, **kwargs: Any) -> None: + self._jwk = kwargs.pop("_jwk", False) + self._not_before: Optional[datetime] = None + self._expires_on: Optional[datetime] = None + self._key_id: Optional[KeyVaultResourceId] = None + + if isinstance(key, KeyVaultKey): + self._key: Union[JsonWebKey, KeyVaultKey, str, None] = key.key + self._key_id = parse_key_vault_id(key.id) + if key.properties._attributes: + self._not_before = key.properties.not_before + self._expires_on = key.properties.expires_on + elif isinstance(key, str): + self._key = None + self._key_id = parse_key_vault_id(key) + if self._key_id.version is None: + self._key_id.version = "" # to avoid an error and get the latest version when getting the key + self._keys_get_forbidden = False + elif self._jwk: + self._key = key + else: + raise ValueError("'key' must be a KeyVaultKey instance or a key ID string") + + if self._jwk: + try: + self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) + self._initialized = True + except Exception as ex: + raise ValueError("The provided jwk is not valid for local cryptography") from ex + else: + self._local_provider = NoLocalCryptography() + self._initialized = False + + self._vault_url = None if (self._jwk or self._key_id is None) else self._key_id.vault_url # type: ignore + super(CryptographyClient, self).__init__( + vault_url=self._vault_url or "vault_url", credential=credential, **kwargs + ) + + @property + def key_id(self) -> Optional[str]: + """The full identifier of the client's key. + + This property may be None when a client is constructed with :func:`from_jwk`. + + :returns: The full identifier of the client's key. + :rtype: str or None + """ + if not self._jwk: + return self._key_id.source_id if self._key_id else None + return cast(JsonWebKey, self._key).kid # type: ignore[attr-defined] + + @property + def vault_url(self) -> Optional[str]: # type: ignore + """The base vault URL of the client's key. + + This property may be None when a client is constructed with :func:`from_jwk`. + + :returns: The base vault URL of the client's key. + :rtype: str or None + """ + return self._vault_url + + @classmethod + def from_jwk(cls, jwk: Union[JsonWebKey, Dict[str, Any]]) -> "CryptographyClient": + """Creates a client that can only perform cryptographic operations locally. + + :param jwk: the key's cryptographic material, as a JsonWebKey or dictionary. + :type jwk: JsonWebKey or Dict[str, Any] + + :returns: A client that can only perform local cryptographic operations. + :rtype: CryptographyClient + """ + if not isinstance(jwk, JsonWebKey): + jwk = JsonWebKey(**jwk) + return cls(jwk, object(), _jwk=True) # type: ignore + + @distributed_trace + def _initialize(self, **kwargs: Any) -> None: + if self._initialized: + return + + # try to get the key material, if we don't have it and aren't forbidden to do so + if not (self._key or self._keys_get_forbidden): + try: + key_bundle = self._client.get_key( + self._key_id.name if self._key_id else None, + self._key_id.version if self._key_id else None, + **kwargs, + ) + key = KeyVaultKey._from_key_bundle(key_bundle) + self._key = key.key + self._key_id = parse_key_vault_id(key.id) # update the key ID in case we didn't have the version before + except HttpResponseError as ex: + # if we got a 403, we don't have keys/get permission and won't try to get the key again + # (other errors may be transient) + self._keys_get_forbidden = ex.status_code == 403 + + # if we have the key material, create a local crypto provider with it + if self._key: + self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) + self._initialized = True + else: + # try to get the key again next time unless we know we're forbidden to do so + self._initialized = self._keys_get_forbidden + + @distributed_trace + def create_rsa_private_key(self) -> KeyVaultRSAPrivateKey: # pylint:disable=client-method-missing-kwargs + """Create an `RSAPrivateKey` implementation backed by this `CryptographyClient`, as a `KeyVaultRSAPrivateKey`. + + The `CryptographyClient` will attempt to download the key, if it hasn't been already, as part of this operation. + + :returns: A `KeyVaultRSAPrivateKey`, which implements `cryptography`'s `RSAPrivateKey` interface. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey + """ + self._initialize() + return KeyVaultRSAPrivateKey(client=self, key_material=cast(JsonWebKey, self._key)) + + @distributed_trace + def create_rsa_public_key(self) -> KeyVaultRSAPublicKey: # pylint:disable=client-method-missing-kwargs + """Create an `RSAPublicKey` implementation backed by this `CryptographyClient`, as a `KeyVaultRSAPublicKey`. + + The `CryptographyClient` will attempt to download the key, if it hasn't been already, as part of this operation. + + :returns: A `KeyVaultRSAPublicKey`, which implements `cryptography`'s `RSAPublicKey` interface. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey + """ + self._initialize() + return KeyVaultRSAPublicKey(client=self, key_material=cast(JsonWebKey, self._key)) + + @distributed_trace + def encrypt( + self, + algorithm: EncryptionAlgorithm, + plaintext: bytes, + *, + iv: Optional[bytes] = None, + additional_authenticated_data: Optional[bytes] = None, + **kwargs: Any, + ) -> EncryptResult: + """Encrypt bytes using the client's key. + + Requires the keys/encrypt permission. This method encrypts only a single block of data, whose size depends on + the key and encryption algorithm. + + :param algorithm: Encryption algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm + :param bytes plaintext: Bytes to encrypt + + :keyword iv: Initialization vector. Required for only AES-CBC(PAD) encryption. If you pass your own IV, + make sure you use a cryptographically random, non-repeating IV. If omitted, an attempt will be made to + generate an IV via `os.urandom `_ for local + cryptography; for remote cryptography, Key Vault will generate an IV. + :paramtype iv: bytes or None + :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use + with AES-GCM encryption. + :paramtype additional_authenticated_data: bytes or None + + :returns: The result of the encryption operation. + :rtype: ~azure.keyvault.keys.crypto.EncryptResult + + :raises ValueError: if parameters that are incompatible with the specified algorithm are provided, or if + generating an IV fails on the current platform. + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START encrypt] + :end-before: [END encrypt] + :caption: Encrypt bytes + :language: python + :dedent: 8 + """ + _validate_arguments( + operation=KeyOperation.encrypt, algorithm=algorithm, iv=iv, aad=additional_authenticated_data + ) + self._initialize(**kwargs) + + if self._local_provider.supports(KeyOperation.encrypt, algorithm): + raise_if_time_invalid(self._not_before, self._expires_on) + try: + return self._local_provider.encrypt(algorithm, plaintext, iv=iv) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local encrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.encrypt}" operation with algorithm "{algorithm}"' + ) + + operation_result = self._client.encrypt( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters( + algorithm=algorithm, value=plaintext, iv=iv, aad=additional_authenticated_data + ), + **kwargs, + ) + + result_iv = operation_result.iv if hasattr(operation_result, "iv") else None + result_tag = operation_result.authentication_tag if hasattr(operation_result, "authentication_tag") else None + result_aad = ( + operation_result.additional_authenticated_data + if hasattr(operation_result, "additional_authenticated_data") + else None + ) + + return EncryptResult( + key_id=self.key_id, + algorithm=algorithm, + ciphertext=operation_result.result, + iv=result_iv, + authentication_tag=result_tag, + additional_authenticated_data=result_aad, + ) + + @distributed_trace + def decrypt( + self, + algorithm: EncryptionAlgorithm, + ciphertext: bytes, + *, + iv: Optional[bytes] = None, + authentication_tag: Optional[bytes] = None, + additional_authenticated_data: Optional[bytes] = None, + **kwargs: Any, + ) -> DecryptResult: + """Decrypt a single block of encrypted data using the client's key. + + Requires the keys/decrypt permission. This method decrypts only a single block of data, whose size depends on + the key and encryption algorithm. + + :param algorithm: Encryption algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm + :param bytes ciphertext: Encrypted bytes to decrypt. Microsoft recommends you not use CBC without first ensuring + the integrity of the ciphertext using, for example, an HMAC. See + https://learn.microsoft.com/dotnet/standard/security/vulnerabilities-cbc-mode for more information. + + :keyword iv: The initialization vector used during encryption. Required for AES decryption. + :paramtype iv: bytes or None + :keyword authentication_tag: The authentication tag generated during encryption. Required for only AES-GCM + decryption. + :paramtype authentication_tag: bytes or None + :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use + with AES-GCM decryption. + :paramtype additional_authenticated_data: bytes or None + + :returns: The result of the decryption operation. + :rtype: ~azure.keyvault.keys.crypto.DecryptResult + + :raises ValueError: If parameters that are incompatible with the specified algorithm are provided. + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START decrypt] + :end-before: [END decrypt] + :caption: Decrypt bytes + :language: python + :dedent: 8 + """ + _validate_arguments( + operation=KeyOperation.decrypt, + algorithm=algorithm, + iv=iv, + tag=authentication_tag, + aad=additional_authenticated_data, + ) + self._initialize(**kwargs) + + if self._local_provider.supports(KeyOperation.decrypt, algorithm): + try: + return self._local_provider.decrypt(algorithm, ciphertext, iv=iv) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local decrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.decrypt}" operation with algorithm "{algorithm}"' + ) + + operation_result = self._client.decrypt( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters( + algorithm=algorithm, value=ciphertext, iv=iv, tag=authentication_tag, aad=additional_authenticated_data + ), + **kwargs, + ) + + return DecryptResult(key_id=self.key_id, algorithm=algorithm, plaintext=operation_result.result) + + @distributed_trace + def wrap_key(self, algorithm: KeyWrapAlgorithm, key: bytes, **kwargs: Any) -> WrapResult: + """Wrap a key with the client's key. + + Requires the keys/wrapKey permission. + + :param algorithm: wrapping algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm + :param bytes key: key to wrap + + :returns: The result of the wrapping operation. + :rtype: ~azure.keyvault.keys.crypto.WrapResult + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START wrap_key] + :end-before: [END wrap_key] + :caption: Wrap a key + :language: python + :dedent: 8 + """ + self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.wrap_key, algorithm): + raise_if_time_invalid(self._not_before, self._expires_on) + try: + return self._local_provider.wrap_key(algorithm, key) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local wrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.wrap_key}" operation with algorithm "{algorithm}"' + ) + + operation_result = self._client.wrap_key( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=key), + **kwargs, + ) + + return WrapResult(key_id=self.key_id, algorithm=algorithm, encrypted_key=operation_result.result) + + @distributed_trace + def unwrap_key(self, algorithm: KeyWrapAlgorithm, encrypted_key: bytes, **kwargs: Any) -> UnwrapResult: + """Unwrap a key previously wrapped with the client's key. + + Requires the keys/unwrapKey permission. + + :param algorithm: wrapping algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm + :param bytes encrypted_key: the wrapped key + + :returns: The result of the unwrapping operation. + :rtype: ~azure.keyvault.keys.crypto.UnwrapResult + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START unwrap_key] + :end-before: [END unwrap_key] + :caption: Unwrap a key + :language: python + :dedent: 8 + """ + self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.unwrap_key, algorithm): + try: + return self._local_provider.unwrap_key(algorithm, encrypted_key) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local unwrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.unwrap_key}" operation with algorithm "{algorithm}"' + ) + + operation_result = self._client.unwrap_key( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=encrypted_key), + **kwargs, + ) + return UnwrapResult(key_id=self.key_id, algorithm=algorithm, key=operation_result.result) + + @distributed_trace + def sign(self, algorithm: SignatureAlgorithm, digest: bytes, **kwargs: Any) -> SignResult: + """Create a signature from a digest using the client's key. + + Requires the keys/sign permission. + + :param algorithm: signing algorithm + :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm + :param bytes digest: hashed bytes to sign + + :returns: The result of the signing operation. + :rtype: ~azure.keyvault.keys.crypto.SignResult + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START sign] + :end-before: [END sign] + :caption: Sign bytes + :language: python + :dedent: 8 + """ + self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.sign, algorithm): + raise_if_time_invalid(self._not_before, self._expires_on) + try: + return self._local_provider.sign(algorithm, digest) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local sign operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.sign}" operation with algorithm "{algorithm}"' + ) + + operation_result = self._client.sign( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeySignParameters(algorithm=algorithm, value=digest), + **kwargs, + ) + + return SignResult(key_id=self.key_id, algorithm=algorithm, signature=operation_result.result) + + @distributed_trace + def verify(self, algorithm: SignatureAlgorithm, digest: bytes, signature: bytes, **kwargs: Any) -> VerifyResult: + """Verify a signature using the client's key. + + Requires the keys/verify permission. + + :param algorithm: verification algorithm + :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm + :param bytes digest: Pre-hashed digest corresponding to **signature**. The hash algorithm used must be + compatible with ``algorithm``. + :param bytes signature: signature to verify + + :returns: The result of the verifying operation. + :rtype: ~azure.keyvault.keys.crypto.VerifyResult + + .. literalinclude:: ../tests/test_examples_crypto.py + :start-after: [START verify] + :end-before: [END verify] + :caption: Verify a signature + :language: python + :dedent: 8 + """ + self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.verify, algorithm): + try: + return self._local_provider.verify(algorithm, digest, signature) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local verify operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.verify}" operation with algorithm "{algorithm}"' + ) + + operation_result = self._client.verify( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyVerifyParameters(algorithm=algorithm, digest=digest, signature=signature), + **kwargs, + ) + + return VerifyResult(key_id=self.key_id, algorithm=algorithm, is_valid=operation_result.value) + + def __enter__(self) -> "CryptographyClient": + self._client.__enter__() + return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py new file mode 100644 index 000000000000..356b72b5edc5 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_enums.py @@ -0,0 +1,67 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from enum import Enum +from azure.core import CaseInsensitiveEnumMeta + + +# pylint: disable=enum-must-be-uppercase +class KeyWrapAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Key wrapping algorithms""" + + aes_128 = "A128KW" + aes_192 = "A192KW" + aes_256 = "A256KW" + # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. + # Microsoft does *not* recommend RSA_OAEP, which is included solely for backwards compatibility. + # RSA_OAEP utilizes SHA1, which has known collision problems. + rsa_oaep = "RSA-OAEP" + rsa_oaep_256 = "RSA-OAEP-256" + # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. + # Microsoft does *not* recommend RSA_1_5, which is included solely for backwards compatibility. + # Cryptographic standards no longer consider RSA with the PKCS#1 v1.5 padding scheme secure for encryption. + rsa1_5 = "RSA1_5" + ckm_aes_key_wrap = "CKM_AES_KEY_WRAP" + ckm_aes_key_wrap_pad = "CKM_AES_KEY_WRAP_PAD" + + +class EncryptionAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Encryption algorithms""" + + # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. + # Microsoft does *not* recommend RSA_OAEP, which is included solely for backwards compatibility. + # RSA_OAEP utilizes SHA1, which has known collision problems. + rsa_oaep = "RSA-OAEP" + rsa_oaep_256 = "RSA-OAEP-256" + # [Not recommended] Microsoft recommends using RSA_OAEP_256 or stronger algorithms for enhanced security. + # Microsoft does *not* recommend RSA_1_5, which is included solely for backwards compatibility. + # Cryptographic standards no longer consider RSA with the PKCS#1 v1.5 padding scheme secure for encryption. + rsa1_5 = "RSA1_5" + a128_gcm = "A128GCM" + a192_gcm = "A192GCM" + a256_gcm = "A256GCM" + a128_cbc = "A128CBC" + a192_cbc = "A192CBC" + a256_cbc = "A256CBC" + a128_cbcpad = "A128CBCPAD" + a192_cbcpad = "A192CBCPAD" + a256_cbcpad = "A256CBCPAD" + + +class SignatureAlgorithm(str, Enum, metaclass=CaseInsensitiveEnumMeta): + """Signature algorithms, described in https://tools.ietf.org/html/rfc7518""" + + ps256 = "PS256" #: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 + ps384 = "PS384" #: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 + ps512 = "PS512" #: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 + rs256 = "RS256" #: RSASSA-PKCS1-v1_5 using SHA-256 + rs384 = "RS384" #: RSASSA-PKCS1-v1_5 using SHA-384 + rs512 = "RS512" #: RSASSA-PKCS1-v1_5 using SHA-512 + es256 = "ES256" #: ECDSA using P-256 and SHA-256 + es384 = "ES384" #: ECDSA using P-384 and SHA-384 + es512 = "ES512" #: ECDSA using P-521 and SHA-512 + es256_k = "ES256K" #: ECDSA using P-256K and SHA-256 + hs256 = "HS256" #: HMAC using SHA-256 + hs384 = "HS384" #: HMAC using SHA-384 + hs512 = "HS512" #: HMAC using SHA-512 diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py new file mode 100644 index 000000000000..880d4cdeb7ae --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/__init__.py @@ -0,0 +1,32 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from .algorithm import ( + Algorithm, + AsymmetricEncryptionAlgorithm, + SymmetricEncryptionAlgorithm, + AuthenticatedSymmetricEncryptionAlgorithm, + SignatureAlgorithm, +) +from .ec_key import EllipticCurveKey +from .key import Key +from .rsa_key import RsaKey +from .symmetric_key import SymmetricKey +from .transform import CryptoTransform, BlockCryptoTransform, AuthenticatedCryptoTransform, SignatureTransform + +__all__ = [ + "Key", + "EllipticCurveKey", + "RsaKey", + "Algorithm", + "AsymmetricEncryptionAlgorithm", + "SymmetricEncryptionAlgorithm", + "AuthenticatedCryptoTransform", + "SignatureAlgorithm", + "CryptoTransform", + "BlockCryptoTransform", + "AuthenticatedSymmetricEncryptionAlgorithm", + "SignatureTransform", + "SymmetricKey", +] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py new file mode 100644 index 000000000000..f19eb2cb0ee0 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/_internal.py @@ -0,0 +1,131 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import codecs +from base64 import b64encode, b64decode + +from cryptography.hazmat.primitives.asymmetric import utils + + +def _bytes_to_int(b): + if not b or not isinstance(b, bytes): + raise ValueError("b must be non-empty byte string") + + return int(codecs.encode(b, "hex"), 16) + + +def _int_to_bytes(i): + h = hex(i) + if len(h) > 1 and h[0:2] == "0x": + h = h[2:] + + # need to strip L in python 2.x + h = h.strip("L") + + if len(h) % 2: + h = "0" + h + return codecs.decode(h, "hex") + + +def _bstr_to_b64url(bstr): + """Serialize bytes into base-64 string. + + :param bytes bstr: Object to be serialized. + + :returns: The base-64 URL encoded string. + :rtype: str + """ + encoded = b64encode(bstr).decode() + return encoded.strip("=").replace("+", "-").replace("/", "_") + + +def _str_to_b64url(s): + """Serialize str into base-64 string. + + :param str s: Object to be serialized. + + :returns: The base-64 URL encoded string. + :rtype: str + """ + return _bstr_to_b64url(s.encode(encoding="utf8")) + + +def _b64_to_bstr(b64str): + """Deserialize base-64 encoded string into string. + + :param str b64str: response string to be deserialized. + + :returns: The decoded bytes. + :rtype: bytes + + :raises: TypeError if string format invalid. + """ + padding = "=" * (3 - (len(b64str) + 3) % 4) + b64str = b64str + padding + encoded = b64str.replace("-", "+").replace("_", "/") + return b64decode(encoded) + + +def _b64_to_str(b64str): + """Deserialize base-64 encoded string into string. + + :param str b64str: response string to be deserialized. + + :returns: The decoded string. + :rtype: str + + :raises: TypeError if string format invalid. + """ + return _b64_to_bstr(b64str).decode("utf8") + + +def _int_to_fixed_length_bigendian_bytes(i, length): + """Convert an integer to a bigendian byte string left-padded with zeroes to a fixed length. + + :param int i: The integer to convert. + :param int length: The length of the desired byte string. + + :returns: A bigendian byte string of length `length`, representing integer `i`. + :rtype: bytes + """ + + b = _int_to_bytes(i) + + if len(b) > length: + raise ValueError(f"{i} is too large to be represented by {length} bytes") + + if len(b) < length: + b = (b"\0" * (length - len(b))) + b + + return b + + +def ecdsa_to_asn1_der(signature): + """ASN.1 DER encode an ECDSA signature. + + :param bytes signature: ECDSA signature encoded according to RFC 7518, i.e. the concatenated big-endian bytes of + two integers (as produced by Key Vault) + + :returns: signature, ASN.1 DER encoded (as expected by ``cryptography``) + :rtype: bytes + """ + mid = len(signature) // 2 + r = _bytes_to_int(signature[:mid]) + s = _bytes_to_int(signature[mid:]) + return utils.encode_dss_signature(r, s) + + +def asn1_der_to_ecdsa(signature, algorithm): + """Convert an ASN.1 DER encoded signature to ECDSA encoding. + + :param bytes signature: an ASN.1 DER encoded ECDSA signature (as produced by ``cryptography``) + :param _Ecdsa algorithm: signing algorithm which produced ``signature`` + + :returns: signature encoded according to RFC 7518 (as expected by Key Vault) + :rtype: bytes + """ + r, s = utils.decode_dss_signature(signature) + r_bytes = _int_to_fixed_length_bigendian_bytes(r, algorithm.coordinate_length) + s_bytes = _int_to_fixed_length_bigendian_bytes(s, algorithm.coordinate_length) + return r_bytes + s_bytes diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py new file mode 100644 index 000000000000..1b2c2446ed6b --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithm.py @@ -0,0 +1,78 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from abc import abstractmethod +from typing import Optional, TYPE_CHECKING, Union + +if TYPE_CHECKING: + from cryptography.hazmat.primitives import hashes + + +_alg_registry = {} + + +class Algorithm(object): + _name: Optional[str] = None + + @classmethod + def name(cls): + return cls._name + + @classmethod + def register(cls): + _alg_registry[cls._name] = cls + + @staticmethod + def resolve(name): + if name not in _alg_registry: + return None + return _alg_registry[name]() + + +class AsymmetricEncryptionAlgorithm(Algorithm): + @abstractmethod + def create_encryptor(self, key): + raise NotImplementedError() + + @abstractmethod + def create_decryptor(self, key): + raise NotImplementedError() + + +class SymmetricEncryptionAlgorithm(Algorithm): + @abstractmethod + def create_encryptor(self, key, iv): + raise NotImplementedError() + + @abstractmethod + def create_decryptor(self, key, iv): + raise NotImplementedError() + + +class AuthenticatedSymmetricEncryptionAlgorithm(Algorithm): # pylint:disable=bad-option-value,name-too-long + @abstractmethod + def create_encryptor(self, key, iv, auth_data, auth_tag): + raise NotImplementedError() + + @abstractmethod + def create_decryptor(self, key, iv, auth_data, auth_tag): + raise NotImplementedError() + + +class SignatureAlgorithm(Algorithm): + _default_hash_algorithm: "Union[hashes.SHA256, hashes.SHA384, hashes.SHA512, None]" = None + + @property + def default_hash_algorithm(self): + return self._default_hash_algorithm + + @abstractmethod + def create_signature_transform(self, key): + raise NotImplementedError() + + +class HashAlgorithm(Algorithm): + @abstractmethod + def create_digest(self): + raise NotImplementedError() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py new file mode 100644 index 000000000000..76c0368acdcf --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/__init__.py @@ -0,0 +1,38 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from .aes_cbc import Aes128Cbc, Aes192Cbc, Aes256Cbc, Aes128CbcPad, Aes192CbcPad, Aes256CbcPad +from .aes_cbc_hmac import Aes128CbcHmacSha256, Aes192CbcHmacSha384, Aes256CbcHmacSha512 +from .aes_kw import AesKw128, AesKw192, AesKw256 +from .ecdsa import Ecdsa256, Es256, Es384, Es512 +from .rsa_encryption import Rsa1_5, RsaOaep, RsaOaep256 +from .rsa_signing import Ps256, Ps384, Ps512, Rs256, Rs384, Rs512 + +__all__ = [ + "Aes128Cbc", + "Aes192Cbc", + "Aes256Cbc", + "Aes128CbcPad", + "Aes192CbcPad", + "Aes256CbcPad", + "Aes128CbcHmacSha256", + "Aes192CbcHmacSha384", + "Aes256CbcHmacSha512", + "AesKw128", + "AesKw192", + "AesKw256", + "Ecdsa256", + "Es256", + "Es384", + "Es512", + "Ps256", + "Ps384", + "Ps512", + "Rsa1_5", + "Rs256", + "Rs384", + "Rs512", + "RsaOaep", + "RsaOaep256", +] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py new file mode 100644 index 000000000000..618ffaf370ae --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc.py @@ -0,0 +1,145 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import padding + +from ..algorithm import SymmetricEncryptionAlgorithm +from ..transform import BlockCryptoTransform + + +# pylint: disable=W0223 + +_CBC_BLOCK_SIZE = 128 + + +class _AesCbcCryptoTransform(BlockCryptoTransform): + def __init__(self, key, iv): + super(_AesCbcCryptoTransform, self).__init__(key) + self._cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + + def transform(self, data): + return self.update(data) + self.finalize() + + def block_size(self): + return _CBC_BLOCK_SIZE + + +class _AesCbcDecryptor(_AesCbcCryptoTransform): + def __init__(self, key, iv, padding_mode): + super(_AesCbcDecryptor, self).__init__(key, iv) + self._ctx = self._cipher.decryptor() + self._padder = padding_mode.unpadder() + + def update(self, data): + decrypted = self._ctx.update(data) + self._ctx.finalize() + return self._padder.update(decrypted) + + def finalize(self): + return self._padder.finalize() + + +class _AesCbcEncryptor(_AesCbcCryptoTransform): + def __init__(self, key, iv, padding_mode): + super(_AesCbcEncryptor, self).__init__(key, iv) + self._ctx = self._cipher.encryptor() + self._padder = padding_mode.padder() + + def update(self, data): + padded = self._padder.update(data) + self._padder.finalize() + return self._ctx.update(padded) + + def finalize(self): + return self._ctx.finalize() + + +class _AesCbc(SymmetricEncryptionAlgorithm): + _key_size = 256 + _block_size = _CBC_BLOCK_SIZE + + def block_size(self): + return self._block_size + + def block_size_in_bytes(self): + return self._block_size >> 3 + + def key_size(self): + return self._key_size + + def key_size_in_bytes(self): + return self._key_size >> 3 + + def create_encryptor(self, key, iv): + key, iv = self._validate_input(key, iv) + + return _AesCbcEncryptor(key, iv, padding.PKCS7(self._block_size)) + + def create_decryptor(self, key, iv): + key, iv = self._validate_input(key, iv) + + return _AesCbcDecryptor(key, iv, padding.PKCS7(self._block_size)) + + def _validate_input(self, key, iv): + if not key: + raise ValueError("A key is required for AES-CBC and AES-CBCPAD encryption and decryption") + if len(key) < self.key_size_in_bytes(): + raise ValueError(f"key must be at least {self.key_size} bits") + + if not iv: + raise ValueError("A 16-byte iv is required for AES-CBC and AES-CBCPAD encryption and decryption") + if not len(iv) == self.block_size_in_bytes(): + raise ValueError(f"iv must be {self.block_size} bits") + + return key[: self.key_size_in_bytes()], iv + + +class _AesCbcPad(_AesCbc): + def create_encryptor(self, key, iv): + key, iv = self._validate_input(key, iv) + + return _AesCbcEncryptor(key, iv, padding.PKCS7(self._block_size)) + + def create_decryptor(self, key, iv): + key, iv = self._validate_input(key, iv) + + return _AesCbcDecryptor(key, iv, padding.PKCS7(self._block_size)) + + +class Aes128Cbc(_AesCbc): + _name = "A128CBC" + _key_size = 128 + + +class Aes128CbcPad(_AesCbcPad): + _name = "A128CBCPAD" + _key_size = 128 + + +class Aes192Cbc(_AesCbc): + _name = "A192CBC" + _key_size = 192 + + +class Aes192CbcPad(_AesCbcPad): + _name = "A192CBCPAD" + _key_size = 192 + + +class Aes256Cbc(_AesCbc): + _name = "A256CBC" + _key_size = 256 + + +class Aes256CbcPad(_AesCbcPad): + _name = "A256CBCPAD" + _key_size = 256 + + +Aes128Cbc.register() +Aes128CbcPad.register() +Aes192Cbc.register() +Aes192CbcPad.register() +Aes256Cbc.register() +Aes256CbcPad.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py new file mode 100644 index 000000000000..4fad959fade9 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_cbc_hmac.py @@ -0,0 +1,149 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from abc import abstractmethod + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import padding, hashes, hmac + +from ..algorithm import AuthenticatedSymmetricEncryptionAlgorithm +from ..transform import AuthenticatedCryptoTransform +from .._internal import _int_to_fixed_length_bigendian_bytes + + +class _AesCbcHmacCryptoTransform(AuthenticatedCryptoTransform): + def __init__(self, key, iv, auth_data, auth_tag): + super(_AesCbcHmacCryptoTransform, self).__init__() + + self._aes_key = key[: len(key) // 2] + self._hmac_key = key[len(key) // 2 :] + hash_algorithm = {256: hashes.SHA256(), 384: hashes.SHA384(), 512: hashes.SHA512()}[len(key) * 8] + + self._cipher = Cipher(algorithms.AES(self._aes_key), modes.CBC(iv), backend=default_backend()) + self._tag = auth_tag or bytearray() + self._hmac = hmac.HMAC(self._hmac_key, hash_algorithm, backend=default_backend()) + self._auth_data_length = _int_to_fixed_length_bigendian_bytes(len(auth_data) * 8, 8) + + # prime the hash + self._hmac.update(auth_data) + self._hmac.update(iv) + + def tag(self): + return self._tag + + def block_size(self): + # return self._cipher.block_size + raise NotImplementedError() + + @abstractmethod + def update(self, data): + raise NotImplementedError() + + @abstractmethod + def finalize(self): + raise NotImplementedError() + + def transform(self, data): + return self.update(data) + self.finalize() + + +class _AesCbcHmacEncryptor(_AesCbcHmacCryptoTransform): + def __init__(self, key, iv, auth_data, auth_tag): + super(_AesCbcHmacEncryptor, self).__init__(key, iv, auth_data, auth_tag) + self._ctx = self._cipher.encryptor() + self._padder = padding.PKCS7(self.block_size).padder() + self._tag[:] = [] + + def update(self, data): + padded = self._padder.update(data) + cipher_text = self._ctx.update(padded) + self._hmac.update(cipher_text) + return cipher_text + + def finalize(self): + padded = self._padder.finalize() + cipher_text = self._ctx.update(padded) + self._ctx.finalize() + self._hmac.update(cipher_text) + self._hmac.update(self._auth_data_length) + self._tag.extend(self._hmac.finalize()[: len(self._hmac_key)]) + return cipher_text + + def block_size(self): + raise NotImplementedError() + + +class _AesCbcHmacDecryptor(_AesCbcHmacCryptoTransform): + def __init__(self, key, iv, auth_data, auth_tag): + super(_AesCbcHmacDecryptor, self).__init__(key, iv, auth_data, auth_tag) + self._ctx = self._cipher.decryptor() + self._padder = padding.PKCS7(self.block_size).unpadder() + + def update(self, data): + self._hmac.update(data) + padded = self._ctx.update(data) + return self._padder.update(padded) + + def finalize(self): + self._hmac.update(self._auth_data_length) + self._hmac.verify(self.tag) + padded = self._ctx.finalize() + return self._padder.update(padded) + self._padder.finalize() + + # override transform from the base so we can verify the entire hash before we start decrypting + def transform(self, data): + self._hmac.update(data) + self._hmac.update(self._auth_data_length) + self._hmac.verify(self.tag) + padded = self._ctx.update(data) + self._ctx.finalize() + return self._padder.update(padded) + self._padder.finalize() + + def block_size(self): + raise NotImplementedError() + + +class _AesCbcHmac(AuthenticatedSymmetricEncryptionAlgorithm): + _key_size = 256 + + @property + def block_size(self): + return self._key_size // 2 + + @property + def block_size_in_bytes(self): + return self.block_size >> 3 + + @property + def key_size(self): + return self._key_size + + @property + def key_size_in_bytes(self): + return self._key_size >> 3 + + def create_encryptor(self, key, iv, auth_data, auth_tag=None): + return _AesCbcHmacEncryptor(key, iv, auth_data, auth_tag) + + def create_decryptor(self, key, iv, auth_data, auth_tag): + return _AesCbcHmacDecryptor(key, iv, auth_data, auth_tag) + + +class Aes128CbcHmacSha256(_AesCbcHmac): + _key_size = 256 + _name = "A128CBC-HS256" + + +class Aes192CbcHmacSha384(_AesCbcHmac): + _key_size = 384 + _name = "A192CBC-HS384" + + +class Aes256CbcHmacSha512(_AesCbcHmac): + _key_size = 512 + _name = "A256CBC-HS512" + + +Aes128CbcHmacSha256.register() +Aes192CbcHmacSha384.register() +Aes256CbcHmacSha512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py new file mode 100644 index 000000000000..4990de7331bb --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/aes_kw.py @@ -0,0 +1,68 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from cryptography.hazmat.primitives.keywrap import aes_key_wrap, aes_key_unwrap +from cryptography.hazmat.backends import default_backend + +from ..algorithm import AsymmetricEncryptionAlgorithm +from ..transform import CryptoTransform +from ..._enums import KeyWrapAlgorithm + + +class _AesKeyWrapTransform(CryptoTransform): + def transform(self, data): + return aes_key_wrap(self._key, data, default_backend()) + + +class _AesKeyUnwrapTransform(CryptoTransform): + def transform(self, data): + return aes_key_unwrap(self._key, data, default_backend()) + + +class _AesKeyWrap(AsymmetricEncryptionAlgorithm): + _key_size = 256 + + @property + def key_size(self): + return self._key_size + + @property + def key_size_in_bytes(self): + return self._key_size >> 3 + + def create_encryptor(self, key): + key = self._validate_input(key) + return _AesKeyWrapTransform(key) + + def create_decryptor(self, key): + key = self._validate_input(key) + return _AesKeyUnwrapTransform(key) + + def _validate_input(self, key): + if not key: + raise ValueError("key") + if len(key) < self.key_size_in_bytes: + raise ValueError(f"key must be at least {self.key_size} bits") + + return key[: self.key_size_in_bytes] + + +class AesKw128(_AesKeyWrap): + _key_size = 128 + _name = "A128KW" + + +class AesKw192(_AesKeyWrap): + _key_size = 192 + _name = "A192KW" + + +class AesKw256(_AesKeyWrap): + _key_size = 256 + _name = KeyWrapAlgorithm.aes_256 + + +AesKw128.register() +AesKw192.register() +AesKw256.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py new file mode 100644 index 000000000000..2c3a6c8bd132 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/ecdsa.py @@ -0,0 +1,60 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import utils + +from ..algorithm import SignatureAlgorithm +from ..transform import SignatureTransform +from ..._enums import SignatureAlgorithm as KeyVaultSignatureAlgorithm + + +class _EcdsaSignatureTransform(SignatureTransform): + def __init__(self, key, hash_algorithm): + super(_EcdsaSignatureTransform, self).__init__() + + self._key = key + self._hash_algorithm = hash_algorithm + + def sign(self, digest): + return self._key.sign(digest, ec.ECDSA(utils.Prehashed(self._hash_algorithm))) + + def verify(self, digest, signature): + return self._key.verify(signature, digest, ec.ECDSA(utils.Prehashed(self._hash_algorithm))) + + +class _Ecdsa(SignatureAlgorithm): + def create_signature_transform(self, key): + return _EcdsaSignatureTransform(key, self.default_hash_algorithm) + + +class Ecdsa256(_Ecdsa): + _name = KeyVaultSignatureAlgorithm.es256_k + _default_hash_algorithm = hashes.SHA256() + coordinate_length = 32 + + +class Es256(_Ecdsa): + _name = KeyVaultSignatureAlgorithm.es256 + _default_hash_algorithm = hashes.SHA256() + coordinate_length = 32 + + +class Es384(_Ecdsa): + _name = KeyVaultSignatureAlgorithm.es384 + _default_hash_algorithm = hashes.SHA384() + coordinate_length = 48 + + +class Es512(_Ecdsa): + _name = KeyVaultSignatureAlgorithm.es512 + _default_hash_algorithm = hashes.SHA512() + coordinate_length = 66 + + +Ecdsa256.register() +Es256.register() +Es384.register() +Es512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py new file mode 100644 index 000000000000..df83c67365d2 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_encryption.py @@ -0,0 +1,79 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding + +from ..algorithm import AsymmetricEncryptionAlgorithm +from ..transform import CryptoTransform +from ..._enums import EncryptionAlgorithm + + +class _Rsa1_5Encryptor(CryptoTransform): + def transform(self, data): + return self._key.encrypt(data, padding.PKCS1v15()) + + +class _Rsa1_5Decryptor(CryptoTransform): + def transform(self, data): + return self._key.decrypt(data, padding.PKCS1v15()) + + +class Rsa1_5(AsymmetricEncryptionAlgorithm): # pylint:disable=client-incorrect-naming-convention + _name = EncryptionAlgorithm.rsa1_5 + + def create_encryptor(self, key): + return _Rsa1_5Encryptor(key) + + def create_decryptor(self, key): + return _Rsa1_5Decryptor(key) + + +class _RsaOaepDecryptor(CryptoTransform): + def __init__(self, key, hash_cls): + self._hash_cls = hash_cls + super(_RsaOaepDecryptor, self).__init__(key) + + def transform(self, data): + oaep_padding = padding.OAEP( + mgf=padding.MGF1(algorithm=self._hash_cls()), algorithm=self._hash_cls(), label=None + ) + return self._key.decrypt(data, oaep_padding) + + +class _RsaOaepEncryptor(CryptoTransform): + def __init__(self, key, hash_cls): + self._hash_cls = hash_cls + super(_RsaOaepEncryptor, self).__init__(key) + + def transform(self, data): + oaep_padding = padding.OAEP( + mgf=padding.MGF1(algorithm=self._hash_cls()), algorithm=self._hash_cls(), label=None + ) + return self._key.encrypt(data, oaep_padding) + + +class RsaOaep(AsymmetricEncryptionAlgorithm): + _name = EncryptionAlgorithm.rsa_oaep + + def create_encryptor(self, key): + return _RsaOaepEncryptor(key, hashes.SHA1) + + def create_decryptor(self, key): + return _RsaOaepDecryptor(key, hashes.SHA1) + + +class RsaOaep256(AsymmetricEncryptionAlgorithm): + _name = EncryptionAlgorithm.rsa_oaep_256 + + def create_encryptor(self, key): + return _RsaOaepEncryptor(key, hashes.SHA256) + + def create_decryptor(self, key): + return _RsaOaepDecryptor(key, hashes.SHA256) + + +Rsa1_5.register() +RsaOaep.register() +RsaOaep256.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py new file mode 100644 index 000000000000..984befca583a --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/rsa_signing.py @@ -0,0 +1,75 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, utils + +from ..algorithm import SignatureAlgorithm +from ..transform import SignatureTransform +from ..._enums import SignatureAlgorithm as KeyVaultSignatureAlgorithm + + +class RsaSignatureTransform(SignatureTransform): + def __init__(self, key, padding_function, hash_algorithm): + super(RsaSignatureTransform, self).__init__() + self._key = key + self._padding_function = padding_function + self._hash_algorithm = hash_algorithm + + def sign(self, digest): + return self._key.sign(digest, self._padding_function(digest), utils.Prehashed(self._hash_algorithm)) + + def verify(self, digest, signature): + self._key.verify(signature, digest, self._padding_function(digest), utils.Prehashed(self._hash_algorithm)) + + +class RsaSsaPkcs1v15(SignatureAlgorithm): + def create_signature_transform(self, key): + return RsaSignatureTransform(key, lambda _: padding.PKCS1v15(), self._default_hash_algorithm) + + +class RsaSsaPss(SignatureAlgorithm): + def create_signature_transform(self, key): + return RsaSignatureTransform(key, self._get_padding, self._default_hash_algorithm) + + def _get_padding(self, digest): + return padding.PSS(mgf=padding.MGF1(self._default_hash_algorithm), salt_length=len(digest)) + + +class Ps256(RsaSsaPss): + _name = KeyVaultSignatureAlgorithm.ps256 + _default_hash_algorithm = hashes.SHA256() + + +class Ps384(RsaSsaPss): + _name = KeyVaultSignatureAlgorithm.ps384 + _default_hash_algorithm = hashes.SHA384() + + +class Ps512(RsaSsaPss): + _name = KeyVaultSignatureAlgorithm.ps512 + _default_hash_algorithm = hashes.SHA512() + + +class Rs256(RsaSsaPkcs1v15): + _name = KeyVaultSignatureAlgorithm.rs256 + _default_hash_algorithm = hashes.SHA256() + + +class Rs384(RsaSsaPkcs1v15): + _name = KeyVaultSignatureAlgorithm.rs384 + _default_hash_algorithm = hashes.SHA384() + + +class Rs512(RsaSsaPkcs1v15): + _name = KeyVaultSignatureAlgorithm.rs512 + _default_hash_algorithm = hashes.SHA512() + + +Ps256.register() +Ps384.register() +Ps512.register() +Rs256.register() +Rs384.register() +Rs512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py new file mode 100644 index 000000000000..34e4a0bc3bbc --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/algorithms/sha_2.py @@ -0,0 +1,53 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Union, Type + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes + +from ..algorithm import HashAlgorithm +from ..transform import DigestTransform + + +class _Sha2DigestTransform(DigestTransform): + def __init__(self, algorithm): + super(_Sha2DigestTransform, self).__init__() + self._digest = hashes.Hash(algorithm=algorithm, backend=default_backend()) + + def update(self, data): + return self._digest.update(data) + + def finalize(self, data): + return self._digest.finalize() + + +class _Sha2HashAlgorithm(HashAlgorithm): + + _algorithm_cls: Union[Type[hashes.SHA256], Type[hashes.SHA384], Type[hashes.SHA512], None] = None + + def create_digest(self): + return _Sha2DigestTransform(self._algorithm_cls()) # pylint:disable=not-callable + + +class Sha256(_Sha2HashAlgorithm): + _algorithm_cls = hashes.SHA256 + _name = "SHA256" + + +class Sha384(_Sha2HashAlgorithm): + _algorithm_cls = hashes.SHA384 + _name = "SHA384" + + +class Sha512(_Sha2HashAlgorithm): + _algorithm_cls = hashes.SHA512 + _name = "SHA512" + + +Sha256.register() + +Sha384.register() + +Sha512.register() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py new file mode 100644 index 000000000000..481a6fd45241 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/ec_key.py @@ -0,0 +1,106 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import uuid + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.ec import ( + EllipticCurvePrivateKey, + EllipticCurvePrivateNumbers, + EllipticCurvePublicNumbers, + SECP256R1, + SECP384R1, + SECP521R1, + SECP256K1, +) + +from ._internal import _bytes_to_int, asn1_der_to_ecdsa, ecdsa_to_asn1_der +from .key import Key +from .algorithms.ecdsa import Es256, Es512, Es384, Ecdsa256 +from ... import KeyCurveName + +_crypto_crv_to_kv_crv = { + "secp256r1": KeyCurveName.p_256, + "secp384r1": KeyCurveName.p_384, + "secp521r1": KeyCurveName.p_521, + "secp256k1": KeyCurveName.p_256_k, +} +_kv_crv_to_crypto_cls = { + KeyCurveName.p_256: SECP256R1, + KeyCurveName.p_256_k: SECP256K1, + KeyCurveName.p_384: SECP384R1, + KeyCurveName.p_521: SECP521R1, + "SECP256K1": SECP256K1, # "SECP256K1" is from Key Vault 2016-10-01 +} +_curve_to_default_algorithm = { + KeyCurveName.p_256: Es256.name(), + KeyCurveName.p_256_k: Ecdsa256.name(), + KeyCurveName.p_384: Es384.name(), + KeyCurveName.p_521: Es512.name(), + "SECP256K1": Ecdsa256.name(), # "SECP256K1" is from Key Vault 2016-10-01 +} + + +class EllipticCurveKey(Key): + _supported_signature_algorithms = frozenset(_curve_to_default_algorithm.values()) + + def __init__(self, x, y, d=None, kid=None, curve=None): + super(EllipticCurveKey, self).__init__() + + self._kid = kid or str(uuid.uuid4()) + self._default_algorithm = _curve_to_default_algorithm[curve] + curve_cls = _kv_crv_to_crypto_cls[curve] + + public_numbers = EllipticCurvePublicNumbers(x, y, curve_cls()) + self._public_key = public_numbers.public_key(default_backend()) + self._private_key = None + if d is not None: + private_numbers = EllipticCurvePrivateNumbers(d, public_numbers) + self._private_key = private_numbers.private_key(default_backend()) + + @classmethod + def from_jwk(cls, jwk): + if jwk.kty not in ("EC", "EC-HSM"): + raise ValueError("The specified key must be of type 'EC' or 'EC-HSM'") + + if not jwk.x or not jwk.y: + raise ValueError("jwk must have values for 'x' and 'y'") + + x = _bytes_to_int(jwk.x) + y = _bytes_to_int(jwk.y) + d = _bytes_to_int(jwk.d) if jwk.d is not None else None + return cls(x, y, d, kid=jwk.kid, curve=jwk.crv) + + def is_private_key(self): + return isinstance(self._private_key, EllipticCurvePrivateKey) + + def decrypt(self, cipher_text, **kwargs): + raise NotImplementedError("Local decryption isn't supported with elliptic curve keys") + + def encrypt(self, plain_text, **kwargs): + raise NotImplementedError("Local encryption isn't supported with elliptic curve keys") + + def wrap_key(self, key, **kwargs): + raise NotImplementedError("Local key wrapping isn't supported with elliptic curve keys") + + def unwrap_key(self, encrypted_key, **kwargs): + raise NotImplementedError("Local key unwrapping isn't supported with elliptic curve keys") + + def sign(self, digest, **kwargs): + algorithm = self._get_algorithm("sign", **kwargs) + signer = algorithm.create_signature_transform(self._private_key) + signature = signer.sign(digest) + ecdsa_signature = asn1_der_to_ecdsa(signature, algorithm) + return ecdsa_signature + + def verify(self, digest, signature, **kwargs): + algorithm = self._get_algorithm("verify", **kwargs) + signer = algorithm.create_signature_transform(self._public_key) + asn1_signature = ecdsa_to_asn1_der(signature) + try: + signer.verify(digest, asn1_signature) + return True + except InvalidSignature: + return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py new file mode 100644 index 000000000000..82298feb1654 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/key.py @@ -0,0 +1,94 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from abc import ABCMeta, abstractmethod +from typing import Any, FrozenSet + +from .algorithm import Algorithm + + +class Key(object, metaclass=ABCMeta): + _supported_encryption_algorithms: FrozenSet[Any] = frozenset([]) + _supported_key_wrap_algorithms: FrozenSet[Any] = frozenset([]) + _supported_signature_algorithms: FrozenSet[Any] = frozenset([]) + + def __init__(self): + self._kid = None + + @property + def default_encryption_algorithm(self): + return None + + @property + def default_key_wrap_algorithm(self): + return None + + @property + def default_signature_algorithm(self): + return None + + @property + def supported_encryption_algorithms(self): + return self._supported_encryption_algorithms + + @property + def supported_key_wrap_algorithms(self): + return self._supported_key_wrap_algorithms + + @property + def supported_signature_algorithms(self): + return self._supported_signature_algorithms + + @property + def kid(self): + return self._kid + + @abstractmethod + def is_private_key(self): + pass + + @abstractmethod + def decrypt(self, cipher_text, **kwargs): + raise NotImplementedError() + + @abstractmethod + def encrypt(self, plain_text, **kwargs): + raise NotImplementedError() + + @abstractmethod + def wrap_key(self, key, **kwargs): + raise NotImplementedError() + + @abstractmethod + def unwrap_key(self, encrypted_key, **kwargs): + raise NotImplementedError() + + @abstractmethod + def sign(self, digest, **kwargs): + raise NotImplementedError() + + @abstractmethod + def verify(self, digest, signature, **kwargs): + raise NotImplementedError() + + def _get_algorithm(self, op, **kwargs): + default_algorithm, supported_algorithms = { + "encrypt": (self.default_encryption_algorithm, self.supported_encryption_algorithms), + "decrypt": (self.default_encryption_algorithm, self.supported_encryption_algorithms), + "wrapKey": (self.default_key_wrap_algorithm, self.supported_key_wrap_algorithms), + "unwrapKey": (self.default_key_wrap_algorithm, self.supported_key_wrap_algorithms), + "sign": (self.default_signature_algorithm, self.supported_signature_algorithms), + "verify": (self.default_signature_algorithm, self.supported_signature_algorithms), + }[op] + + algorithm = kwargs.get("algorithm", default_algorithm) + + if not isinstance(algorithm, Algorithm): + algorithm = Algorithm.resolve(algorithm) + + if not algorithm or not supported_algorithms or algorithm.name() not in supported_algorithms: + raise ValueError(f"unsupported algorithm '{algorithm}'") + + return algorithm diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py new file mode 100644 index 000000000000..0d3f21df4257 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/rsa_key.py @@ -0,0 +1,228 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import uuid + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, + RSAPrivateNumbers, + RSAPublicNumbers, + generate_private_key, + rsa_crt_dmp1, + rsa_crt_dmq1, + rsa_crt_iqmp, +) + +from ._internal import _bytes_to_int, _int_to_bytes +from .key import Key +from .algorithms import Ps256, Ps384, Ps512, Rsa1_5, RsaOaep, RsaOaep256, Rs256, Rs384, Rs512 +from ... import JsonWebKey, KeyOperation + + +class RsaKey(Key): # pylint:disable=too-many-public-methods + PUBLIC_KEY_DEFAULT_OPS = [KeyOperation.encrypt, KeyOperation.wrap_key, KeyOperation.verify] + PRIVATE_KEY_DEFAULT_OPS = PUBLIC_KEY_DEFAULT_OPS + [ + KeyOperation.decrypt, + KeyOperation.unwrap_key, + KeyOperation.sign, + ] + + _supported_encryption_algorithms = frozenset((Rsa1_5.name(), RsaOaep.name(), RsaOaep256.name())) + _supported_key_wrap_algorithms = frozenset((Rsa1_5.name(), RsaOaep.name(), RsaOaep256.name())) + _supported_signature_algorithms = frozenset( + ( + Ps256.name(), + Ps384.name(), + Ps512.name(), + Rs256.name(), + Rs384.name(), + Rs512.name(), + ) + ) + + def __init__(self, kid=None): + super(RsaKey, self).__init__() + self._kid = kid + self.kty = None + self.key_ops = None + self._rsa_impl = None + + @property + def n(self): + return _int_to_bytes(self._public_key_material().n) + + @property + def e(self): + return _int_to_bytes(self._public_key_material().e) + + @property + def p(self): + return _int_to_bytes(self._private_key_material().p) if self.is_private_key() else None + + @property + def q(self): + return _int_to_bytes(self._private_key_material().q) if self.is_private_key() else None + + @property + def b(self): + return _int_to_bytes(self._private_key_material().b) if self.is_private_key() else None + + @property + def d(self): + return _int_to_bytes(self._private_key_material().d) if self.is_private_key() else None + + @property + def dq(self): + return _int_to_bytes(self._private_key_material().dmq1) if self.is_private_key() else None + + @property + def dp(self): + return _int_to_bytes(self._private_key_material().dmp1) if self.is_private_key() else None + + @property + def qi(self): + return _int_to_bytes(self._private_key_material().iqmp) if self.is_private_key() else None + + @property + def private_key(self): + return self._rsa_impl if self.is_private_key() else None + + @property + def public_key(self): + return self._rsa_impl.public_key() if self.is_private_key() else self._rsa_impl + + @staticmethod + def generate(kid=None, kty="RSA", size=2048, e=65537): + key = RsaKey() + key.kid = kid or str(uuid.uuid4()) + key.kty = kty + key.key_ops = RsaKey.PRIVATE_KEY_DEFAULT_OPS + # pylint:disable=protected-access + key._rsa_impl = generate_private_key(public_exponent=e, key_size=size, backend=default_backend()) + return key + + @classmethod + def from_jwk(cls, jwk): + if jwk.kty not in ("RSA", "RSA-HSM"): + raise ValueError('The specified jwk must have a key type of "RSA" or "RSA-HSM"') + + if not jwk.n or not jwk.e: + raise ValueError("Invalid RSA jwk, both n and e must be have values") + + rsa_key = cls(kid=jwk.kid) + rsa_key.kty = jwk.kty + rsa_key.key_ops = jwk.key_ops + + pub = RSAPublicNumbers(n=_bytes_to_int(jwk.n), e=_bytes_to_int(jwk.e)) + + # if the private key values are specified construct a private key + # only the secret primes and private exponent are needed as other fields can be calculated + if jwk.p and jwk.q and jwk.d: + # convert the values of p, q, and d from bytes to int + p = _bytes_to_int(jwk.p) + q = _bytes_to_int(jwk.q) + d = _bytes_to_int(jwk.d) + + # convert or compute the remaining private key numbers + dmp1 = _bytes_to_int(jwk.dp) if jwk.dp else rsa_crt_dmp1(private_exponent=d, p=p) + dmq1 = _bytes_to_int(jwk.dq) if jwk.dq else rsa_crt_dmq1(private_exponent=d, q=q) + iqmp = _bytes_to_int(jwk.qi) if jwk.qi else rsa_crt_iqmp(p=p, q=q) + + # create the private key from the jwk key values + priv = RSAPrivateNumbers(p=p, q=q, d=d, dmp1=dmp1, dmq1=dmq1, iqmp=iqmp, public_numbers=pub) + key_impl = priv.private_key(default_backend()) + + # if the necessary private key values are not specified create the public key + else: + key_impl = pub.public_key(default_backend()) + + rsa_key._rsa_impl = key_impl + + return rsa_key + + def to_jwk(self, include_private=False): + jwk = JsonWebKey( + kid=self.kid, + kty=self.kty, + key_ops=self.key_ops if include_private else RsaKey.PUBLIC_KEY_DEFAULT_OPS, + n=self.n, + e=self.e, + ) + + if include_private: + jwk.q = self.q + jwk.p = self.p + jwk.d = self.d + jwk.dq = self.dq + jwk.dp = self.dp + jwk.qi = self.qi + + return jwk + + @property + def default_encryption_algorithm(self): + return RsaOaep.name() + + @property + def default_key_wrap_algorithm(self): + return RsaOaep.name() + + @property + def default_signature_algorithm(self): + return Rs256.name() + + def encrypt(self, plain_text, **kwargs): + algorithm = self._get_algorithm("encrypt", **kwargs) + encryptor = algorithm.create_encryptor(self.public_key) + return encryptor.transform(plain_text) + + def decrypt(self, cipher_text, **kwargs): + if not self.is_private_key(): + raise NotImplementedError("The current RsaKey does not support decrypt") + + algorithm = self._get_algorithm("decrypt", **kwargs) + decryptor = algorithm.create_decryptor(self.private_key) + return decryptor.transform(cipher_text) + + def sign(self, digest, **kwargs): + if not self.is_private_key(): + raise NotImplementedError("The current RsaKey does not support sign") + + algorithm = self._get_algorithm("sign", **kwargs) + signer = algorithm.create_signature_transform(self.private_key) + return signer.sign(digest) + + def verify(self, digest, signature, **kwargs): + algorithm = self._get_algorithm("verify", **kwargs) + signer = algorithm.create_signature_transform(self.public_key) + try: + # cryptography's verify methods return None, and raise when verification fails + signer.verify(digest, signature) + return True + except InvalidSignature: + return False + + def wrap_key(self, key, **kwargs): + algorithm = self._get_algorithm("wrapKey", **kwargs) + encryptor = algorithm.create_encryptor(self.public_key) + return encryptor.transform(key) + + def unwrap_key(self, encrypted_key, **kwargs): + if not self.is_private_key(): + raise NotImplementedError("The current RsaKey does not support unwrap") + + algorithm = self._get_algorithm("unwrapKey", **kwargs) + decryptor = algorithm.create_decryptor(self.private_key) + return decryptor.transform(encrypted_key) + + def is_private_key(self): + return isinstance(self._rsa_impl, RSAPrivateKey) + + def _public_key_material(self): + return self.public_key.public_numbers() + + def _private_key_material(self): + return self.private_key.private_numbers() if self.private_key else None diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py new file mode 100644 index 000000000000..1682b1ac4bd0 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/symmetric_key.py @@ -0,0 +1,125 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import uuid +import os + +from azure.core.exceptions import AzureError +from .key import Key +from .algorithms.aes_cbc import Aes256CbcPad, Aes192CbcPad, Aes128CbcPad +from .algorithms.aes_cbc_hmac import Aes256CbcHmacSha512, Aes192CbcHmacSha384 +from .algorithms.aes_kw import AesKw256, AesKw192, AesKw128 + +key_size_128 = 128 >> 3 +key_size_192 = 192 >> 3 +key_size_256 = 256 >> 3 +key_size_384 = 384 >> 3 +key_size_512 = 512 >> 3 + +_default_key_size = key_size_256 + +_supported_key_sizes = [key_size_128, key_size_192, key_size_256, key_size_384, key_size_512] + +_default_enc_alg_by_size = { + key_size_128: Aes128CbcPad.name(), + key_size_192: Aes192CbcPad.name(), + key_size_256: Aes256CbcPad.name(), + key_size_384: Aes192CbcHmacSha384.name(), + key_size_512: Aes256CbcHmacSha512.name(), +} + +_default_kw_alg_by_size = { + key_size_128: AesKw128.name(), + key_size_192: AesKw192.name(), + key_size_256: AesKw256.name(), + key_size_384: AesKw256.name(), + key_size_512: AesKw256.name(), +} + + +def raise_if_incorrect_key_size(algorithm, key_size): + if algorithm._key_size >> 3 != key_size: # pylint:disable=protected-access + raise AzureError("Invalid AES encryption algorithm for key size. The algorithm must match the size of the key.") + + +class SymmetricKey(Key): + def __init__(self, kid=None, key_bytes=None, key_size=None): + super(SymmetricKey, self).__init__() + + self._kid = kid or str(uuid.uuid4()) + + if not key_bytes: + key_size = key_size or _default_key_size + + if key_size not in _supported_key_sizes: + raise ValueError("The key size must be 128, 192, 256, 384 or 512 bits of data") + + key_bytes = os.urandom(key_size) + + if len(key_bytes) not in _supported_key_sizes: + raise ValueError("The key size must be 128, 192, 256, 384 or 512 bits of data") + + self._key = key_bytes + + supported_encryption_algorithms = [] + supported_key_wrap_algorithms = [] + key_size = len(self._key) + if key_size >= key_size_128: + supported_encryption_algorithms.append(Aes128CbcPad.name()) + supported_key_wrap_algorithms.append(AesKw128.name()) + if key_size >= key_size_192: + supported_encryption_algorithms.append(Aes192CbcPad.name()) + supported_key_wrap_algorithms.append(AesKw192.name()) + if key_size >= key_size_256: + supported_encryption_algorithms.append(Aes256CbcPad.name()) + supported_key_wrap_algorithms.append(AesKw256.name()) + self._supported_encryption_algorithms = frozenset(supported_encryption_algorithms) + self._supported_key_wrap_algorithms = frozenset(supported_key_wrap_algorithms) + + def is_private_key(self): + return True + + @classmethod + def from_jwk(cls, jwk): + return cls(kid=jwk.kid, key_bytes=jwk.k) + + @property + def kid(self): + return self._kid + + @property + def default_encryption_algorithm(self): + return _default_enc_alg_by_size[len(self._key)] + + @property + def default_key_wrap_algorithm(self): + return _default_kw_alg_by_size[len(self._key)] + + def encrypt(self, plain_text, iv, **kwargs): # pylint:disable=arguments-differ + algorithm = self._get_algorithm("encrypt", **kwargs) + raise_if_incorrect_key_size(algorithm, len(self._key)) + encryptor = algorithm.create_encryptor(key=self._key, iv=iv) + return encryptor.transform(plain_text) + + def decrypt(self, cipher_text, iv, **kwargs): # pylint:disable=arguments-differ + algorithm = self._get_algorithm("decrypt", **kwargs) + raise_if_incorrect_key_size(algorithm, len(self._key)) + decryptor = algorithm.create_decryptor(key=self._key, iv=iv) + return decryptor.transform(cipher_text) + + def wrap_key(self, key, **kwargs): + algorithm = self._get_algorithm("wrapKey", **kwargs) + encryptor = algorithm.create_encryptor(key=self._key) + return encryptor.transform(key) + + def unwrap_key(self, encrypted_key, **kwargs): + algorithm = self._get_algorithm("unwrapKey", **kwargs) + decryptor = algorithm.create_decryptor(key=self._key) + return decryptor.transform(encrypted_key) + + def sign(self, digest, **kwargs): + raise NotImplementedError("Local signing isn't supported with symmetric keys") + + def verify(self, digest, signature, **kwargs): + raise NotImplementedError("Local signature verification isn't supported with symmetric keys") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py new file mode 100644 index 000000000000..3a24f7ab1a7f --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_internal/transform.py @@ -0,0 +1,61 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +from abc import ABCMeta, abstractmethod + + +class CryptoTransform(object, metaclass=ABCMeta): + def __init__(self, key): + self._key = key + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._key = None + + @abstractmethod + def transform(self, data): + raise NotImplementedError() + + +class BlockCryptoTransform(CryptoTransform): + @abstractmethod + def block_size(self): + raise NotImplementedError() + + @abstractmethod + def update(self, data): + raise NotImplementedError() + + @abstractmethod + def finalize(self): + raise NotImplementedError() + + +class AuthenticatedCryptoTransform(object, metaclass=ABCMeta): + @abstractmethod + def tag(self): + raise NotImplementedError() + + +class SignatureTransform(object, metaclass=ABCMeta): + @abstractmethod + def sign(self, digest): + raise NotImplementedError() + + @abstractmethod + def verify(self, digest, signature): + raise NotImplementedError() + + +class DigestTransform(object, metaclass=ABCMeta): + @abstractmethod + def update(self, data): + raise NotImplementedError() + + @abstractmethod + def finalize(self, data): + raise NotImplementedError() diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py new file mode 100644 index 000000000000..4e879040759b --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_key_validity.py @@ -0,0 +1,16 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from datetime import datetime, timezone +from typing import Optional + + +def raise_if_time_invalid(not_before: Optional[datetime], expires_on: Optional[datetime]) -> None: + now = datetime.now(timezone.utc) + if (not_before and expires_on) and not not_before <= now <= expires_on: + raise ValueError(f"This client's key is useable only between {not_before} and {expires_on} (UTC)") + if not_before and not_before > now: + raise ValueError(f"This client's key is not useable until {not_before} (UTC)") + if expires_on and expires_on <= now: + raise ValueError(f"This client's key expired at {expires_on} (UTC)") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py new file mode 100644 index 000000000000..d65d152f6240 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -0,0 +1,621 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from __future__ import annotations +from typing import Any, cast, Optional, NoReturn, Union, TYPE_CHECKING + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS, MGF1 +from cryptography.hazmat.primitives.asymmetric.rsa import ( + rsa_crt_dmp1, + rsa_crt_dmq1, + rsa_crt_iqmp, + rsa_recover_prime_factors, + RSAPrivateKey, + RSAPrivateNumbers, + RSAPublicKey, + RSAPublicNumbers, +) +from cryptography.hazmat.primitives.asymmetric.utils import Prehashed +from cryptography.hazmat.primitives.hashes import Hash, HashAlgorithm, SHA1, SHA256, SHA384, SHA512 +from cryptography.hazmat.primitives.serialization import ( + Encoding, + KeySerializationEncryption, + PrivateFormat, + PublicFormat, +) + +from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm +from .._models import JsonWebKey + +if TYPE_CHECKING: + # Import client only during TYPE_CHECKING to avoid circular dependency + from ._client import CryptographyClient + + +SIGN_ALGORITHM_MAP = { + SHA256: SignatureAlgorithm.rs256, + SHA384: SignatureAlgorithm.rs384, + SHA512: SignatureAlgorithm.rs512, +} +OAEP_MAP = {SHA1: EncryptionAlgorithm.rsa_oaep, SHA256: EncryptionAlgorithm.rsa_oaep_256} +PSS_MAP = { + SignatureAlgorithm.rs256: SignatureAlgorithm.ps256, + SignatureAlgorithm.rs384: SignatureAlgorithm.ps384, + SignatureAlgorithm.rs512: SignatureAlgorithm.ps512, +} + + +def get_encryption_algorithm(padding: AsymmetricPadding) -> EncryptionAlgorithm: + """Maps an `AsymmetricPadding` to an encryption algorithm. + + :param padding: The padding to use. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + + :returns: The corresponding Key Vault encryption algorithm. + :rtype: EncryptionAlgorithm + """ + if isinstance(padding, OAEP): + # Public algorithm property was only added in https://github.com/pyca/cryptography/pull/9582 + # _algorithm property has been available in every version of the OAEP class, so we use it as a backup + try: + algorithm = padding.algorithm # type: ignore[attr-defined] + except AttributeError: + algorithm = padding._algorithm # pylint:disable=protected-access + mapped_algorithm = OAEP_MAP.get(type(algorithm)) + if mapped_algorithm is None: + raise ValueError(f"Unsupported algorithm: {algorithm.name}") + + # Public mgf property was added at the same time as algorithm + try: + mgf = padding.mgf # type: ignore[attr-defined] + except AttributeError: + mgf = padding._mgf # pylint:disable=protected-access + if not isinstance(mgf, MGF1): + raise ValueError(f"Unsupported MGF: {mgf}") + + elif isinstance(padding, PKCS1v15): + mapped_algorithm = EncryptionAlgorithm.rsa1_5 + else: + raise ValueError(f"Unsupported padding: {padding.name}") + + return mapped_algorithm + + +def get_signature_algorithm(padding: AsymmetricPadding, algorithm: HashAlgorithm) -> SignatureAlgorithm: + """Maps an `AsymmetricPadding` and `HashAlgorithm` to a signature algorithm. + + :param padding: The padding to use. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + :param algorithm: The algorithm to use. + :type algorithm: ~cryptography.hazmat.primitives.hashes.HashAlgorithm + + :returns: The corresponding Key Vault signature algorithm. + :rtype: SignatureAlgorithm + """ + mapped_algorithm = SIGN_ALGORITHM_MAP.get(type(algorithm)) + if mapped_algorithm is None: + raise ValueError(f"Unsupported algorithm: {algorithm.name}") + + # If PSS padding is requested, use the PSS equivalent algorithm + if isinstance(padding, PSS): + mapped_algorithm = PSS_MAP.get(mapped_algorithm) + + # Public mgf property was only added in https://github.com/pyca/cryptography/pull/9582 + # _mgf property has been available in every version of the PSS class, so we use it as a backup + try: + mgf = padding.mgf # type: ignore[attr-defined] + except AttributeError: + mgf = padding._mgf # pylint:disable=protected-access + if not isinstance(mgf, MGF1): + raise ValueError(f"Unsupported MGF: {mgf}") + + # The only other padding accepted is PKCS1v15 + elif not isinstance(padding, PKCS1v15): + raise ValueError(f"Unsupported padding: {padding.name}") + + return cast(SignatureAlgorithm, mapped_algorithm) + + +class KeyVaultRSAPublicKey(RSAPublicKey): + """An `RSAPublicKey` implementation based on a key managed by Key Vault. + + This class should not be instantiated directly. Instead, use the + :func:`~azure.keyvault.keys.crypto.CryptographyClient.create_rsa_public_key` method to create a key based on the + client's key. Only synchronous clients and operations are supported at this time. + """ + + def __init__(self, client: "CryptographyClient", key_material: Optional[JsonWebKey] = None) -> None: + self._client: "CryptographyClient" = client + self._key: Optional[JsonWebKey] = key_material + + def encrypt(self, plaintext: bytes, padding: AsymmetricPadding) -> bytes: + """Encrypts the given plaintext. + + :param bytes plaintext: Plaintext to encrypt. + :param padding: The padding to use. Supported paddings are `OAEP` and `PKCS1v15`. For `OAEP` padding, supported + hash algorithms are `SHA1` and `SHA256`. The only supported mask generation function is `MGF1`. See + https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + + :returns: The encrypted ciphertext, as bytes. + :rtype: bytes + """ + mapped_algorithm = get_encryption_algorithm(padding) + result = self._client.encrypt(mapped_algorithm, plaintext) + return result.ciphertext + + @property + def key_size(self) -> int: + """The bit length of the public modulus. + + :returns: The key's size. + :rtype: int + + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(encrypt, verify) can be performed." + ) + + public_key = self.public_numbers().public_key() + return public_key.key_size + + def public_numbers(self) -> RSAPublicNumbers: + """Returns an `RSAPublicNumbers` representing the key's public numbers. + + :returns: The public numbers of the key. + :rtype: RSAPublicNumbers + + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(encrypt, verify) can be performed." + ) + + e = int.from_bytes(self._key.e, "big") # type: ignore[attr-defined] + n = int.from_bytes(self._key.n, "big") # type: ignore[attr-defined] + return RSAPublicNumbers(e, n) + + def public_bytes(self, encoding: Encoding, format: PublicFormat) -> bytes: + """Allows serialization of the key to bytes. + + This function uses the `cryptography` library's implementation. + Encoding (`PEM` or `DER`) and format (`SubjectPublicKeyInfo` or `PKCS1`) are chosen to define the exact + serialization. + + :param encoding: A value from the `Encoding` enum. + :type encoding: ~cryptography.hazmat.primitives.serialization.Encoding + :param format: A value from the `PublicFormat` enum. + :type format: ~cryptography.hazmat.primitives.serialization.PublicFormat + + :returns: The serialized key. + :rtype: bytes + + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(encrypt, verify) can be performed." + ) + + public_key = self.public_numbers().public_key() + return public_key.public_bytes(encoding=encoding, format=format) + + def verify( + self, + signature: bytes, + data: bytes, + padding: AsymmetricPadding, + algorithm: Union[Prehashed, HashAlgorithm], + ) -> None: + """Verifies the signature of the data. + + :param bytes signature: The signature to sign, as bytes. + :param bytes data: The message string that was signed., as bytes. + :param padding: The padding to use. Supported paddings are `PKCS1v15` and `PSS`. For `PSS`, the only supported + mask generation function is `MGF1`. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details + for details. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, + `SHA384`, and `SHA512`. + :type algorithm: ~cryptography.hazmat.primitives.asymmetric.utils.Prehashed or + cryptography.hazmat.primitives.hashes.HashAlgorithm + + :raises InvalidSignature: If the signature does not validate. + """ + if isinstance(algorithm, Prehashed): + raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") + mapped_algorithm = get_signature_algorithm(padding, algorithm) + digest = Hash(algorithm) + digest.update(data) + result = self._client.verify(mapped_algorithm, digest.finalize(), signature) + if not result.is_valid: + raise InvalidSignature(f"The provided signature '{signature!r}' is invalid.") + + def recover_data_from_signature( # type: ignore[override] # Parameter subset + self, + signature: bytes, + padding: AsymmetricPadding, + algorithm: Optional[HashAlgorithm], + ) -> bytes: + # pylint: disable=line-too-long + """Recovers the signed data from the signature. Only supported with `cryptography` version 3.3 and above. + + This function uses the `cryptography` library's implementation. + The data typically contains the digest of the original message string. The `padding` and `algorithm` parameters + must match the ones used when the signature was created for the recovery to succeed. + The `algorithm` parameter can also be set to None to recover all the data present in the signature, without + regard to its format or the hash algorithm used for its creation. + + For `PKCS1v15` padding, this method returns the data after removing the padding layer. For standard signatures + the data contains the full `DigestInfo` structure. For non-standard signatures, any data can be returned, + including zero-length data. + + Normally you should use the `verify()` function to validate the signature. But for some non-standard signature + formats you may need to explicitly recover and validate the signed data. The following are some examples: + + * Some old Thawte and Verisign timestamp certificates without `DigestInfo`. + * Signed MD5/SHA1 hashes in TLS 1.1 or earlier (`RFC 4346 `_, section 4.7). + * IKE version 1 signatures without `DigestInfo` (`RFC 2409 `_, section 5.1). + + :param bytes signature: The signature. + :param padding: An instance of `AsymmetricPadding`. Recovery is only supported with some of the padding types. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + :param algorithm: An instance of `HashAlgorithm`. Can be None to return all the data present in the signature. + :type algorithm: ~cryptography.hazmat.primitives.hashes.HashAlgorithm + + :returns: The signed data. + :rtype: bytes + :raises NotImplementedError: if the local version of `cryptography` doesn't support this method. + :raises ~cryptography.exceptions.InvalidSignature: if the signature is invalid. + :raises ~cryptography.exceptions.UnsupportedAlgorithm: if the signature data recovery is not supported with + the provided `padding` type. + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(encrypt, verify) can be performed." + ) + + public_key = self.public_numbers().public_key() + try: + return public_key.recover_data_from_signature(signature=signature, padding=padding, algorithm=algorithm) + except AttributeError as exc: + raise NotImplementedError( + "This method is only available on `cryptography`>=3.3. Update your package version to use this method." + ) from exc + + def __eq__(self, other: object) -> bool: + """Checks equality. + + :param object other: Another object to compare with this instance. Currently, only comparisons with + `KeyVaultRSAPrivateKey` or `JsonWebKey` instances are supported. + + :returns: True if the objects are equal; False if the objects are unequal or if key material can't be obtained + from Key Vault for comparison. + :rtype: bool + """ + if self._key is None: + return False + + if isinstance(other, KeyVaultRSAPublicKey): + return all(getattr(self._key, field) == getattr(other._key, field) for field in self._key._FIELDS) + if isinstance(other, JsonWebKey): + return all(getattr(self._key, field) == getattr(other, field) for field in self._key._FIELDS) + return False + + def __copy__(self) -> KeyVaultRSAPublicKey: + """Returns this instance since it is treated as immutable. + + :returns: This instance. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey + """ + return self + + def __deepcopy__(self, memo: dict) -> KeyVaultRSAPublicKey: + """Returns this instance since it is treated as immutable. + + :param dict memo: The memo dictionary used by deepcopy. + :returns: This instance. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey + """ + return self + + def verifier( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + self, signature: bytes, padding: AsymmetricPadding, algorithm: HashAlgorithm + ) -> NoReturn: + """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" + raise NotImplementedError() + + +class KeyVaultRSAPrivateKey(RSAPrivateKey): + """An `RSAPrivateKey` implementation based on a key managed by Key Vault. + + This class should not be instantiated directly. Instead, use the + :func:`~azure.keyvault.keys.crypto.CryptographyClient.create_rsa_private_key` method to create a key based on the + client's key. Only synchronous clients and operations are supported at this time. + """ + + def __init__(self, client: "CryptographyClient", key_material: Optional[JsonWebKey]) -> None: + self._client: "CryptographyClient" = client + self._key: Optional[JsonWebKey] = key_material + + def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: + """Decrypts the provided ciphertext. + + :param bytes ciphertext: Encrypted bytes to decrypt. + :param padding: The padding to use. Supported paddings are `OAEP` and `PKCS1v15`. For `OAEP` padding, supported + hash algorithms are `SHA1` and `SHA256`. The only supported mask generation function is `MGF1`. See + https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + + :returns: The decrypted plaintext, as bytes. + :rtype: bytes + """ + mapped_algorithm = get_encryption_algorithm(padding) + result = self._client.decrypt(mapped_algorithm, ciphertext) + return result.plaintext + + @property + def key_size(self) -> int: + """The bit length of the public modulus. + + :returns: The key's size. + :rtype: int + + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(decrypt, sign) can be performed." + ) + + # Key size only requires public modulus, which we can always get + # Relying on private numbers instead would cause issues for keys stored in KV (which doesn't return private key) + return self.public_key().key_size + + def public_key(self) -> KeyVaultRSAPublicKey: + """The `RSAPublicKey` associated with this private key, as a `KeyVaultRSAPublicKey`. + + The public key implementation will use the same underlying cryptography client as this private key. + + :returns: The `KeyVaultRSAPublicKey` associated with the key. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey + """ + return KeyVaultRSAPublicKey(self._client, self._key) + + def sign( # type: ignore[override] # Parameter subset + self, + data: bytes, + padding: AsymmetricPadding, + algorithm: Union[Prehashed, HashAlgorithm], + ) -> bytes: + """Signs the data. + + :param bytes data: The data to sign, as bytes. + :param padding: The padding to use. Supported paddings are `PKCS1v15` and `PSS`. For `PSS`, the only supported + mask generation function is `MGF1`. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details + for details. + :type padding: ~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding + :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, + `SHA384`, and `SHA512`. + :type algorithm: ~cryptography.hazmat.primitives.asymmetric.utils.Prehashed or + cryptography.hazmat.primitives.hashes.HashAlgorithm + + :returns: The signature, as bytes. + :rtype: bytes + """ + if isinstance(algorithm, Prehashed): + raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") + mapped_algorithm = get_signature_algorithm(padding, algorithm) + digest = Hash(algorithm) + digest.update(data) + result = self._client.sign(mapped_algorithm, digest.finalize()) + return result.signature + + def private_numbers(self) -> RSAPrivateNumbers: + """Returns an `RSAPrivateNumbers` representing the key's private numbers. + + :returns: The private numbers of the key. + :rtype: ~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateNumbers + + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(decrypt, sign) can be performed." + ) + + # Fetch public numbers from JWK + e = int.from_bytes(self._key.e, "big") # type: ignore[attr-defined] + n = int.from_bytes(self._key.n, "big") # type: ignore[attr-defined] + public_numbers = RSAPublicNumbers(e, n) + + # Fetch private numbers from JWK + p = int.from_bytes(self._key.p, "big") if self._key.p else None # type: ignore[attr-defined] + q = int.from_bytes(self._key.q, "big") if self._key.q else None # type: ignore[attr-defined] + d = int.from_bytes(self._key.d, "big") if self._key.d else None # type: ignore[attr-defined] + dmp1 = int.from_bytes(self._key.dp, "big") if self._key.dp else None # type: ignore[attr-defined] + dmq1 = int.from_bytes(self._key.dq, "big") if self._key.dq else None # type: ignore[attr-defined] + iqmp = int.from_bytes(self._key.qi, "big") if self._key.qi else None # type: ignore[attr-defined] + + # Calculate any missing attributes + if d is None: + raise ValueError("An 'RSAPrivateNumbers' couldn't be created with the available key material.") + if p is None or q is None: + p, q = rsa_recover_prime_factors(n, e, d) + if dmp1 is None: + dmp1 = rsa_crt_dmp1(d, p) + if dmq1 is None: + dmq1 = rsa_crt_dmq1(d, q) + if iqmp is None: + iqmp = rsa_crt_iqmp(p, q) + + return RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, public_numbers) + + def private_bytes( + self, encoding: Encoding, format: PrivateFormat, encryption_algorithm: KeySerializationEncryption + ) -> bytes: + """Allows serialization of the key to bytes. + + This function uses the `cryptography` library's implementation. + Encoding (`PEM` or `DER`) and format (`TraditionalOpenSSL`, `OpenSSH`, or `PKCS8`) and encryption algorithm + (such as `BestAvailableEncryption` or `NoEncryption`) are chosen to define the exact serialization. + + :param encoding: A value from the `Encoding` enum. + :type encoding: ~cryptography.hazmat.primitives.serialization.Encoding + :param format: A value from the `PrivateFormat` enum. + :type format: ~cryptography.hazmat.primitives.serialization.PrivateFormat + :param encryption_algorithm: An instance of an object conforming to the `KeySerializationEncryption` interface. + :type encryption_algorithm: ~cryptography.hazmat.primitives.serialization.KeySerializationEncryption + + :returns: The serialized key. + :rtype: bytes + + :raises ValueError: if the client is unable to obtain the key material from Key Vault. + """ + if self._key is None: + raise ValueError( + "Key material could not be obtained from Key Vault. Only remote cryptographic operations " + "(decrypt, sign) can be performed." + ) + + try: + private_numbers = self.private_numbers() + except ValueError as exc: + raise ValueError("Insufficient key material to serialize the private key.") from exc + private_key = private_numbers.private_key() + return private_key.private_bytes(encoding=encoding, format=format, encryption_algorithm=encryption_algorithm) + + def signer( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + self, padding: AsymmetricPadding, algorithm: HashAlgorithm + ) -> NoReturn: + """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" + raise NotImplementedError() + + def __copy__(self) -> KeyVaultRSAPrivateKey: + """Returns this instance since it is treated as immutable. + + :returns: This instance. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey + """ + return self + + def __deepcopy__(self, memo: dict) -> KeyVaultRSAPrivateKey: + """Returns this instance since it is treated as immutable. + + :param dict memo: The memo dictionary used by deepcopy. + :returns: This instance. + :rtype: ~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey + """ + return self + + +class DecryptResult: + """The result of a decrypt operation. + + :param str key_id: The encryption key's Key Vault identifier + :param algorithm: The encryption algorithm used + :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm + :param bytes plaintext: The decrypted bytes + """ + + def __init__(self, key_id: Optional[str], algorithm: EncryptionAlgorithm, plaintext: bytes) -> None: + self.key_id = key_id + self.algorithm = algorithm + self.plaintext = plaintext + + +class EncryptResult: + """The result of an encrypt operation. + + :param str key_id: The encryption key's Key Vault identifier + :param algorithm: The encryption algorithm used + :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm + :param bytes ciphertext: The encrypted bytes + + :keyword bytes iv: Initialization vector for symmetric algorithms + :keyword bytes authentication_tag: The tag to authenticate when performing decryption with an authenticated + algorithm + :keyword bytes additional_authenticated_data: Additional data to authenticate but not encrypt/decrypt when using an + authenticated algorithm + """ + + def __init__(self, key_id: Optional[str], algorithm: EncryptionAlgorithm, ciphertext: bytes, **kwargs: Any) -> None: + self.key_id = key_id + self.algorithm = algorithm + self.ciphertext = ciphertext + self.iv = kwargs.pop("iv", None) + self.tag = kwargs.pop("authentication_tag", None) + self.aad = kwargs.pop("additional_authenticated_data", None) + + +class SignResult: + """The result of a sign operation. + + :param str key_id: The signing key's Key Vault identifier + :param algorithm: The signature algorithm used + :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm + :param bytes signature: + """ + + def __init__(self, key_id: Optional[str], algorithm: SignatureAlgorithm, signature: bytes) -> None: + self.key_id = key_id + self.algorithm = algorithm + self.signature = signature + + +class VerifyResult: + """The result of a verify operation. + + :param str key_id: The signing key's Key Vault identifier + :param bool is_valid: Whether the signature is valid + :param algorithm: The signature algorithm used + :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm + """ + + def __init__(self, key_id: Optional[str], is_valid: bool, algorithm: SignatureAlgorithm) -> None: + self.key_id = key_id + self.is_valid = is_valid + self.algorithm = algorithm + + +class UnwrapResult: + """The result of an unwrap key operation. + + :param str key_id: Key encryption key's Key Vault identifier + :param algorithm: The key wrap algorithm used + :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm + :param bytes key: The unwrapped key + """ + + def __init__(self, key_id: Optional[str], algorithm: KeyWrapAlgorithm, key: bytes) -> None: + self.key_id = key_id + self.algorithm = algorithm + self.key = key + + +class WrapResult: + """The result of a wrap key operation. + + :param str key_id: The wrapping key's Key Vault identifier + :param algorithm: The key wrap algorithm used + :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm + :param bytes encrypted_key: The encrypted key bytes + """ + + def __init__(self, key_id: Optional[str], algorithm: KeyWrapAlgorithm, encrypted_key: bytes) -> None: + self.key_id = key_id + self.algorithm = algorithm + self.encrypted_key = encrypted_key diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py new file mode 100644 index 000000000000..8c146d1a1fca --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/__init__.py @@ -0,0 +1,36 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import TYPE_CHECKING + +from .ec import EllipticCurveCryptographyProvider +from .local_provider import LocalCryptographyProvider +from .rsa import RsaCryptographyProvider +from .symmetric import SymmetricCryptographyProvider +from ... import KeyType + +if TYPE_CHECKING: + from ... import JsonWebKey + + +def get_local_cryptography_provider(key: "JsonWebKey") -> LocalCryptographyProvider: + if key.kty in (KeyType.ec, KeyType.ec_hsm): # type: ignore[attr-defined] + return EllipticCurveCryptographyProvider(key) + if key.kty in (KeyType.rsa, KeyType.rsa_hsm): # type: ignore[attr-defined] + return RsaCryptographyProvider(key) + if key.kty in (KeyType.oct, KeyType.oct_hsm): # type: ignore[attr-defined] + return SymmetricCryptographyProvider(key) + + raise ValueError(f'Unsupported key type "{key.kty}"') # type: ignore[attr-defined] + + +class NoLocalCryptography(LocalCryptographyProvider): + def __init__(self): # pylint:disable=super-init-not-called + return + + def supports(self, operation, algorithm): + return False + + def _get_internal_key(self, key): + return None diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py new file mode 100644 index 000000000000..d72dd505a1a2 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/ec.py @@ -0,0 +1,34 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import TYPE_CHECKING + +from .local_provider import LocalCryptographyProvider +from .._internal import EllipticCurveKey +from ... import KeyOperation, KeyType + +if TYPE_CHECKING: + from .local_provider import Algorithm + from .._internal import Key + from ... import JsonWebKey + +_PRIVATE_KEY_OPERATIONS = frozenset((KeyOperation.decrypt, KeyOperation.sign, KeyOperation.unwrap_key)) + + +class EllipticCurveCryptographyProvider(LocalCryptographyProvider): + def _get_internal_key(self, key: "JsonWebKey") -> "Key": + if key.kty not in (KeyType.ec, KeyType.ec_hsm): # type: ignore[attr-defined] + raise ValueError('"key" must be an EC or EC-HSM key') + return EllipticCurveKey.from_jwk(key) + + def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: + if operation in _PRIVATE_KEY_OPERATIONS and not self._internal_key.is_private_key(): + return False + if operation in (KeyOperation.decrypt, KeyOperation.encrypt): + return algorithm in self._internal_key.supported_encryption_algorithms + if operation in (KeyOperation.unwrap_key, KeyOperation.wrap_key): + return algorithm in self._internal_key.supported_key_wrap_algorithms + if operation in (KeyOperation.sign, KeyOperation.verify): + return algorithm in self._internal_key.supported_signature_algorithms + return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py new file mode 100644 index 000000000000..6e0edd2f526c --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/local_provider.py @@ -0,0 +1,104 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import abc +import os +from typing import Optional, TYPE_CHECKING, Union + +from azure.core.exceptions import AzureError + +from .. import DecryptResult, EncryptResult, SignResult, UnwrapResult, VerifyResult, WrapResult +from ... import KeyOperation + +ABC = abc.ABC + +if TYPE_CHECKING: + from .._internal.key import Key + from .. import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm + from ... import JsonWebKey + + Algorithm = Union[EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm] + + +class LocalCryptographyProvider(ABC): + def __init__(self, key: "JsonWebKey") -> None: + self._allowed_ops = frozenset(key.key_ops or []) # type: ignore[attr-defined] + self._internal_key = self._get_internal_key(key) + self._key = key + + @abc.abstractmethod + def _get_internal_key(self, key: "JsonWebKey") -> "Key": + pass + + @abc.abstractmethod + def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: + pass + + @property + def key_id(self) -> "Optional[str]": + """The full identifier of the provider's key. + + :returns: The full identifier of the provider's key. + :rtype: str or None + """ + return self._key.kid # type: ignore[attr-defined] + + def _raise_if_unsupported(self, operation: KeyOperation, algorithm: "Algorithm") -> None: + if not self.supports(operation, algorithm): + raise NotImplementedError( + f'This key does not support the "{operation}" operation with algorithm "{algorithm}"' + ) + if operation not in self._allowed_ops: + raise AzureError(f'This key does not allow the "{operation}" operation') + + def encrypt( + self, algorithm: "EncryptionAlgorithm", plaintext: bytes, iv: "Optional[bytes]" = None + ) -> EncryptResult: + self._raise_if_unsupported(KeyOperation.encrypt, algorithm) + + # If an IV isn't provided with AES-CBCPAD encryption, try to create one + if iv is None and algorithm.value.endswith("CBCPAD"): + try: + iv = os.urandom(16) + except NotImplementedError as ex: + raise ValueError( + "An IV could not be generated on this OS. Please provide your own cryptographically random, " + "non-repeating IV for local cryptography." + ) from ex + + ciphertext = self._internal_key.encrypt(plaintext, algorithm=algorithm.value, iv=iv) + return EncryptResult( + key_id=self._key.kid, algorithm=algorithm, ciphertext=ciphertext, iv=iv # type: ignore[attr-defined] + ) + + def decrypt( + self, algorithm: "EncryptionAlgorithm", ciphertext: bytes, iv: "Optional[bytes]" = None + ) -> DecryptResult: + self._raise_if_unsupported(KeyOperation.decrypt, algorithm) + plaintext = self._internal_key.decrypt(ciphertext, iv=iv, algorithm=algorithm.value) + return DecryptResult( + key_id=self._key.kid, algorithm=algorithm, plaintext=plaintext # type: ignore[attr-defined] + ) + + def wrap_key(self, algorithm: "KeyWrapAlgorithm", key: bytes) -> "WrapResult": + self._raise_if_unsupported(KeyOperation.wrap_key, algorithm) + encrypted_key = self._internal_key.wrap_key(key, algorithm=algorithm.value) + return WrapResult( + key_id=self._key.kid, algorithm=algorithm, encrypted_key=encrypted_key # type: ignore[attr-defined] + ) + + def unwrap_key(self, algorithm: "KeyWrapAlgorithm", encrypted_key: bytes) -> "UnwrapResult": + self._raise_if_unsupported(KeyOperation.unwrap_key, algorithm) + unwrapped_key = self._internal_key.unwrap_key(encrypted_key, algorithm=algorithm.value) + return UnwrapResult(key_id=self._key.kid, algorithm=algorithm, key=unwrapped_key) # type: ignore[attr-defined] + + def sign(self, algorithm: "SignatureAlgorithm", digest: bytes) -> "SignResult": + self._raise_if_unsupported(KeyOperation.sign, algorithm) + signature = self._internal_key.sign(digest, algorithm=algorithm.value) + return SignResult(key_id=self._key.kid, algorithm=algorithm, signature=signature) # type: ignore[attr-defined] + + def verify(self, algorithm: "SignatureAlgorithm", digest: bytes, signature: bytes) -> "VerifyResult": + self._raise_if_unsupported(KeyOperation.verify, algorithm) + is_valid = self._internal_key.verify(digest, signature, algorithm=algorithm.value) + return VerifyResult(key_id=self._key.kid, algorithm=algorithm, is_valid=is_valid) # type: ignore[attr-defined] diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py new file mode 100644 index 000000000000..4394cc2a9b51 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/rsa.py @@ -0,0 +1,34 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import TYPE_CHECKING + +from .local_provider import LocalCryptographyProvider +from .._internal import RsaKey +from ... import KeyOperation, KeyType + +if TYPE_CHECKING: + from .local_provider import Algorithm + from .._internal import Key + from ... import JsonWebKey + +_PRIVATE_KEY_OPERATIONS = frozenset((KeyOperation.decrypt, KeyOperation.sign, KeyOperation.unwrap_key)) + + +class RsaCryptographyProvider(LocalCryptographyProvider): + def _get_internal_key(self, key: "JsonWebKey") -> "Key": + if key.kty not in (KeyType.rsa, KeyType.rsa_hsm): # type: ignore[attr-defined] + raise ValueError('"key" must be an RSA or RSA-HSM key') + return RsaKey.from_jwk(key) + + def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: + if operation in _PRIVATE_KEY_OPERATIONS and not self._internal_key.is_private_key(): + return False + if operation in (KeyOperation.decrypt, KeyOperation.encrypt): + return algorithm in self._internal_key.supported_encryption_algorithms + if operation in (KeyOperation.unwrap_key, KeyOperation.wrap_key): + return algorithm in self._internal_key.supported_key_wrap_algorithms + if operation in (KeyOperation.sign, KeyOperation.verify): + return algorithm in self._internal_key.supported_signature_algorithms + return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py new file mode 100644 index 000000000000..3a5f473b36c1 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_providers/symmetric.py @@ -0,0 +1,28 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import TYPE_CHECKING + +from .local_provider import LocalCryptographyProvider +from .._internal import SymmetricKey +from ... import KeyOperation, KeyType + +if TYPE_CHECKING: + from .local_provider import Algorithm + from .._internal import Key + from ... import JsonWebKey + + +class SymmetricCryptographyProvider(LocalCryptographyProvider): + def _get_internal_key(self, key: "JsonWebKey") -> "Key": + if key.kty not in (KeyType.oct, KeyType.oct_hsm): # type: ignore[attr-defined] + raise ValueError('"key" must be an oct or oct-HSM (symmetric) key') + return SymmetricKey.from_jwk(key) + + def supports(self, operation: KeyOperation, algorithm: "Algorithm") -> bool: + if operation in (KeyOperation.decrypt, KeyOperation.encrypt): + return algorithm in self._internal_key.supported_encryption_algorithms + if operation in (KeyOperation.unwrap_key, KeyOperation.wrap_key): + return algorithm in self._internal_key.supported_key_wrap_algorithms + return False diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py new file mode 100644 index 000000000000..3a8c0f5ee127 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/__init__.py @@ -0,0 +1,50 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from typing import Any, List, Optional + +from ._client import CryptographyClient + + +__all__ = [ + "CryptographyClient", +] + + +def __dir__() -> List[str]: + return __all__ + + +# Allow importing these types for backwards compatibility, but exclude indexing types that shouldn't be in aio namespace + + +def __getattr__(name: str): + requested: Optional[Any] = None + if name == "EncryptionAlgorithm": + from .. import EncryptionAlgorithm + + requested = EncryptionAlgorithm + if name == "KeyWrapAlgorithm": + from .. import KeyWrapAlgorithm + + requested = KeyWrapAlgorithm + if name == "SignatureAlgorithm": + from .. import SignatureAlgorithm + + requested = SignatureAlgorithm + if name == "EncryptResult": + from .. import EncryptResult + + requested = EncryptResult + if name == "SignResult": + from .. import SignResult + + requested = SignResult + if name == "WrapResult": + from .. import WrapResult + + requested = WrapResult + if requested: + return requested + raise AttributeError(f"module 'azure.keyvault.keys.crypto.aio' has no attribute {name}") diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py new file mode 100644 index 000000000000..13111932ebbe --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/aio/_client.py @@ -0,0 +1,503 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from datetime import datetime +import logging +from typing import Any, cast, Dict, Optional, Union + +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.exceptions import HttpResponseError +from azure.core.tracing.decorator_async import distributed_trace_async + +from .. import ( + DecryptResult, + EncryptionAlgorithm, + EncryptResult, + KeyWrapAlgorithm, + SignatureAlgorithm, + SignResult, + VerifyResult, + UnwrapResult, + WrapResult, +) +from .._client import _validate_arguments +from .._key_validity import raise_if_time_invalid +from .._providers import get_local_cryptography_provider, NoLocalCryptography +from ... import KeyOperation +from ..._models import JsonWebKey, KeyVaultKey +from ..._shared import AsyncKeyVaultClientBase, KeyVaultResourceId, parse_key_vault_id + +_LOGGER = logging.getLogger(__name__) + + +class CryptographyClient(AsyncKeyVaultClientBase): + """Performs cryptographic operations using Azure Key Vault keys. + + This client will perform operations locally when it's intialized with the necessary key material or is able to get + that material from Key Vault. When the required key material is unavailable, cryptographic operations are performed + by the Key Vault service. + + :param key: Either a azure.keyvault.keys.KeyVaultKey instance as returned by + :func:`~azure.keyvault.keys.aio.KeyClient.get_key`, or a string. + If a string, the value must be the identifier of an Azure Key Vault key. Including a version is recommended. + :type key: str or azure.keyvault.keys.KeyVaultKey + :param credential: An object which can provide an access token for the vault, such as a credential from + :mod:`azure.identity.aio` + :type credential: ~azure.core.credentials_async.AsyncTokenCredential + + :keyword api_version: Version of the service API to use. Defaults to the most recent. + :paramtype api_version: ~azure.keyvault.keys.ApiVersion or str + :keyword bool verify_challenge_resource: Whether to verify the authentication challenge resource matches the Key + Vault or Managed HSM domain. Defaults to True. + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START create_client] + :end-before: [END create_client] + :caption: Create a CryptographyClient + :language: python + :dedent: 8 + """ + + # pylint:disable=protected-access + + def __init__(self, key: Union[KeyVaultKey, str], credential: AsyncTokenCredential, **kwargs: Any) -> None: + self._jwk = kwargs.pop("_jwk", False) + self._not_before: Optional[datetime] = None + self._expires_on: Optional[datetime] = None + self._key_id: Optional[KeyVaultResourceId] = None + + if isinstance(key, KeyVaultKey): + self._key: Union[JsonWebKey, KeyVaultKey, str, None] = key.key + self._key_id = parse_key_vault_id(key.id) + if key.properties._attributes: + self._not_before = key.properties.not_before + self._expires_on = key.properties.expires_on + elif isinstance(key, str): + self._key = None + self._key_id = parse_key_vault_id(key) + if self._key_id.version is None: + self._key_id.version = "" # to avoid an error and get the latest version when getting the key + self._keys_get_forbidden = False + elif self._jwk: + self._key = key + else: + raise ValueError("'key' must be a KeyVaultKey instance or a key ID string") + + if self._jwk: + try: + self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) + self._initialized = True + except Exception as ex: + raise ValueError("The provided jwk is not valid for local cryptography") from ex + else: + self._local_provider = NoLocalCryptography() + self._initialized = False + + self._vault_url = None if (self._jwk or self._key_id is None) else self._key_id.vault_url # type: ignore + super().__init__(vault_url=self._vault_url or "vault_url", credential=credential, **kwargs) + + @property + def key_id(self) -> Optional[str]: + """The full identifier of the client's key. + + This property may be None when a client is constructed with :func:`from_jwk`. + + :returns: The full identifier of the client's key. + :rtype: str or None + """ + if not self._jwk: + return self._key_id.source_id if self._key_id else None + return cast(JsonWebKey, self._key).kid # type: ignore[attr-defined] + + @property + def vault_url(self) -> Optional[str]: # type: ignore + """The base vault URL of the client's key. + + This property may be None when a client is constructed with :func:`from_jwk`. + + :returns: The base vault URL of the client's key. + :rtype: str or None + """ + return self._vault_url + + @classmethod + def from_jwk(cls, jwk: Union[JsonWebKey, Dict[str, Any]]) -> "CryptographyClient": + """Creates a client that can only perform cryptographic operations locally. + + :param jwk: the key's cryptographic material, as a JsonWebKey or dictionary. + :type jwk: JsonWebKey or Dict[str, Any] + + :returns: A client that can only perform local cryptographic operations. + :rtype: CryptographyClient + """ + if not isinstance(jwk, JsonWebKey): + jwk = JsonWebKey(**jwk) + return cls(jwk, object(), _jwk=True) # type: ignore + + @distributed_trace_async + async def _initialize(self, **kwargs: Any) -> None: + if self._initialized: + return + + # try to get the key material, if we don't have it and aren't forbidden to do so + if not (self._key or self._keys_get_forbidden): + try: + key_bundle = await self._client.get_key( + self._key_id.name if self._key_id else None, + self._key_id.version if self._key_id else None, + **kwargs, + ) + key = KeyVaultKey._from_key_bundle(key_bundle) + self._key = key.key + self._key_id = parse_key_vault_id(key.id) # update the key ID in case we didn't have the version before + except HttpResponseError as ex: + # if we got a 403, we don't have keys/get permission and won't try to get the key again + # (other errors may be transient) + self._keys_get_forbidden = ex.status_code == 403 + + # if we have the key material, create a local crypto provider with it + if self._key: + self._local_provider = get_local_cryptography_provider(cast(JsonWebKey, self._key)) + self._initialized = True + else: + # try to get the key again next time unless we know we're forbidden to do so + self._initialized = self._keys_get_forbidden + + @distributed_trace_async + async def encrypt( + self, + algorithm: EncryptionAlgorithm, + plaintext: bytes, + *, + iv: Optional[bytes] = None, + additional_authenticated_data: Optional[bytes] = None, + **kwargs: Any, + ) -> EncryptResult: + """Encrypt bytes using the client's key. + + Requires the keys/encrypt permission. This method encrypts only a single block of data, whose size depends on + the key and encryption algorithm. + + :param algorithm: Encryption algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm + :param bytes plaintext: Bytes to encrypt + + :keyword iv: Initialization vector. Required for only AES-CBC(PAD) encryption. If you pass your own IV, + make sure you use a cryptographically random, non-repeating IV. If omitted, an attempt will be made to + generate an IV via `os.urandom `_ for local + cryptography; for remote cryptography, Key Vault will generate an IV. + :paramtype iv: bytes or None + :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use + with AES-GCM encryption. + :paramtype additional_authenticated_data: bytes or None + + :returns: The result of the encryption operation. + :rtype: ~azure.keyvault.keys.crypto.EncryptResult + + :raises ValueError: if parameters that are incompatible with the specified algorithm are provided, or if + generating an IV fails on the current platform. + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START encrypt] + :end-before: [END encrypt] + :caption: Encrypt bytes + :language: python + :dedent: 8 + """ + _validate_arguments( + operation=KeyOperation.encrypt, algorithm=algorithm, iv=iv, aad=additional_authenticated_data + ) + await self._initialize(**kwargs) + + if self._local_provider.supports(KeyOperation.encrypt, algorithm): + raise_if_time_invalid(self._not_before, self._expires_on) + try: + return self._local_provider.encrypt(algorithm, plaintext, iv=iv) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local encrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.encrypt}" operation with algorithm "{algorithm}"' + ) + + operation_result = await self._client.encrypt( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters( + algorithm=algorithm, value=plaintext, iv=iv, aad=additional_authenticated_data + ), + **kwargs, + ) + + result_iv = operation_result.iv if hasattr(operation_result, "iv") else None + result_tag = operation_result.authentication_tag if hasattr(operation_result, "authentication_tag") else None + result_aad = ( + operation_result.additional_authenticated_data + if hasattr(operation_result, "additional_authenticated_data") + else None + ) + + return EncryptResult( + key_id=self.key_id, + algorithm=algorithm, + ciphertext=operation_result.result, + iv=result_iv, + authentication_tag=result_tag, + additional_authenticated_data=result_aad, + ) + + @distributed_trace_async + async def decrypt( + self, + algorithm: EncryptionAlgorithm, + ciphertext: bytes, + *, + iv: Optional[bytes] = None, + authentication_tag: Optional[bytes] = None, + additional_authenticated_data: Optional[bytes] = None, + **kwargs: Any, + ) -> DecryptResult: + """Decrypt a single block of encrypted data using the client's key. + + Requires the keys/decrypt permission. This method decrypts only a single block of data, whose size depends on + the key and encryption algorithm. + + :param algorithm: Encryption algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.EncryptionAlgorithm + :param bytes ciphertext: Encrypted bytes to decrypt. Microsoft recommends you not use CBC without first ensuring + the integrity of the ciphertext using, for example, an HMAC. See + https://learn.microsoft.com/dotnet/standard/security/vulnerabilities-cbc-mode for more information. + + :keyword iv: The initialization vector used during encryption. Required for AES decryption. + :paramtype iv: bytes or None + :keyword authentication_tag: The authentication tag generated during encryption. Required for only AES-GCM + decryption. + :paramtype authentication_tag: bytes or None + :keyword additional_authenticated_data: Optional data that is authenticated but not encrypted. For use + with AES-GCM decryption. + :paramtype additional_authenticated_data: bytes or None + + :returns: The result of the decryption operation. + :rtype: ~azure.keyvault.keys.crypto.DecryptResult + + :raises ValueError: If parameters that are incompatible with the specified algorithm are provided. + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START decrypt] + :end-before: [END decrypt] + :caption: Decrypt bytes + :language: python + :dedent: 8 + """ + _validate_arguments( + operation=KeyOperation.decrypt, + algorithm=algorithm, + iv=iv, + tag=authentication_tag, + aad=additional_authenticated_data, + ) + await self._initialize(**kwargs) + + if self._local_provider.supports(KeyOperation.decrypt, algorithm): + try: + return self._local_provider.decrypt(algorithm, ciphertext, iv=iv) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local decrypt operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.decrypt}" operation with algorithm "{algorithm}"' + ) + + operation_result = await self._client.decrypt( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters( + algorithm=algorithm, value=ciphertext, iv=iv, tag=authentication_tag, aad=additional_authenticated_data + ), + **kwargs, + ) + + return DecryptResult(key_id=self.key_id, algorithm=algorithm, plaintext=operation_result.result) + + @distributed_trace_async + async def wrap_key(self, algorithm: KeyWrapAlgorithm, key: bytes, **kwargs: Any) -> WrapResult: + """Wrap a key with the client's key. + + Requires the keys/wrapKey permission. + + :param algorithm: wrapping algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm + :param bytes key: key to wrap + + :returns: The result of the wrapping operation. + :rtype: ~azure.keyvault.keys.crypto.WrapResult + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START wrap_key] + :end-before: [END wrap_key] + :caption: Wrap a key + :language: python + :dedent: 8 + """ + await self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.wrap_key, algorithm): + raise_if_time_invalid(self._not_before, self._expires_on) + try: + return self._local_provider.wrap_key(algorithm, key) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local wrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.wrap_key}" operation with algorithm "{algorithm}"' + ) + + operation_result = await self._client.wrap_key( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=key), + **kwargs, + ) + + return WrapResult(key_id=self.key_id, algorithm=algorithm, encrypted_key=operation_result.result) + + @distributed_trace_async + async def unwrap_key(self, algorithm: KeyWrapAlgorithm, encrypted_key: bytes, **kwargs: Any) -> UnwrapResult: + """Unwrap a key previously wrapped with the client's key. + + Requires the keys/unwrapKey permission. + + :param algorithm: wrapping algorithm to use + :type algorithm: ~azure.keyvault.keys.crypto.KeyWrapAlgorithm + :param bytes encrypted_key: the wrapped key + + :returns: The result of the unwrapping operation. + :rtype: ~azure.keyvault.keys.crypto.UnwrapResult + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START unwrap_key] + :end-before: [END unwrap_key] + :caption: Unwrap a key + :language: python + :dedent: 8 + """ + await self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.unwrap_key, algorithm): + try: + return self._local_provider.unwrap_key(algorithm, encrypted_key) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local unwrap operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.unwrap_key}" operation with algorithm "{algorithm}"' + ) + + operation_result = await self._client.unwrap_key( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyOperationsParameters(algorithm=algorithm, value=encrypted_key), + **kwargs, + ) + + return UnwrapResult(key_id=self.key_id, algorithm=algorithm, key=operation_result.result) + + @distributed_trace_async + async def sign(self, algorithm: SignatureAlgorithm, digest: bytes, **kwargs: Any) -> SignResult: + """Create a signature from a digest using the client's key. + + Requires the keys/sign permission. + + :param algorithm: signing algorithm + :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm + :param bytes digest: hashed bytes to sign + + :returns: The result of the signing operation. + :rtype: ~azure.keyvault.keys.crypto.SignResult + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START sign] + :end-before: [END sign] + :caption: Sign bytes + :language: python + :dedent: 8 + """ + await self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.sign, algorithm): + raise_if_time_invalid(self._not_before, self._expires_on) + try: + return self._local_provider.sign(algorithm, digest) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local sign operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.sign}" operation with algorithm "{algorithm}"' + ) + + operation_result = await self._client.sign( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeySignParameters(algorithm=algorithm, value=digest), + **kwargs, + ) + + return SignResult(key_id=self.key_id, algorithm=algorithm, signature=operation_result.result) + + @distributed_trace_async + async def verify( + self, algorithm: SignatureAlgorithm, digest: bytes, signature: bytes, **kwargs: Any + ) -> VerifyResult: + """Verify a signature using the client's key. + + Requires the keys/verify permission. + + :param algorithm: verification algorithm + :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm + :param bytes digest: Pre-hashed digest corresponding to **signature**. The hash algorithm used must be + compatible with ``algorithm``. + :param bytes signature: signature to verify + + :returns: The result of the verifying operation. + :rtype: ~azure.keyvault.keys.crypto.VerifyResult + + .. literalinclude:: ../tests/test_examples_crypto_async.py + :start-after: [START verify] + :end-before: [END verify] + :caption: Verify a signature + :language: python + :dedent: 8 + """ + await self._initialize(**kwargs) + if self._local_provider.supports(KeyOperation.verify, algorithm): + try: + return self._local_provider.verify(algorithm, digest, signature) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning("Local verify operation failed: %s", ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + if self._jwk: + raise + elif self._jwk: + raise NotImplementedError( + f'This key does not support the "{KeyOperation.verify}" operation with algorithm "{algorithm}"' + ) + + operation_result = await self._client.verify( + key_name=self._key_id.name if self._key_id else None, + key_version=self._key_id.version if self._key_id else None, + parameters=self._models.KeyVerifyParameters(algorithm=algorithm, digest=digest, signature=signature), + **kwargs, + ) + + return VerifyResult(key_id=self.key_id, algorithm=algorithm, is_valid=operation_result.value) + + async def __aenter__(self) -> "CryptographyClient": + await self._client.__aenter__() + return self diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/py.typed b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/py.typed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/keyvault/azure-keyvault-keys/pyproject.toml b/sdk/keyvault/azure-keyvault-keys/pyproject.toml index aa0744fc3775..5da059fdcf7e 100644 --- a/sdk/keyvault/azure-keyvault-keys/pyproject.toml +++ b/sdk/keyvault/azure-keyvault-keys/pyproject.toml @@ -1,14 +1,18 @@ +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# Code generated by Microsoft (R) Python Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. +# -------------------------------------------------------------------------- + [build-system] -requires = [ - "setuptools>=77.0.3", - "wheel", -] +requires = ["setuptools>=77.0.3", "wheel"] build-backend = "setuptools.build_meta" [project] name = "azure-keyvault-keys" authors = [ - { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" }, + { name = "Microsoft Corporation", email = "azpysdkhelp@microsoft.com" }, ] description = "Microsoft Corporation Azure Key Vault Keys Client Library for Python" license = "MIT" @@ -17,16 +21,15 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -requires-python = ">=3.10" -keywords = [ - "azure", - "azure sdk", -] +requires-python = ">=3.9" +keywords = ["azure", "azure sdk"] + dependencies = [ "isodate>=0.6.1", "azure-core>=1.37.0", @@ -34,22 +37,15 @@ dependencies = [ "cryptography>=44.0.2", ] dynamic = [ - "version", - "readme", +"version", "readme" ] [project.urls] repository = "https://github.com/Azure/azure-sdk-for-python" -[tool.setuptools.dynamic.version] -attr = "azure.keyvault.keys._generated._version.VERSION" - -[tool.setuptools.dynamic.readme] -file = [ - "README.md", - "CHANGELOG.md", -] -content-type = "text/markdown" +[tool.setuptools.dynamic] +version = {attr = "azure.keyvault.keys._version.VERSION"} +readme = {file = ["README.md", "CHANGELOG.md"], content-type = "text/markdown"} [tool.setuptools.packages.find] exclude = [ @@ -60,13 +56,10 @@ exclude = [ "doc*", "azure", "azure.keyvault", - "azure.keyvault.keys", ] [tool.setuptools.package-data] -pytyped = [ - "py.typed", -] +pytyped = ["py.typed"] [tool.azure-sdk-build] pyright = false @@ -74,6 +67,3 @@ pyright = false [tool.azure-sdk-conda] in_bundle = true bundle_name = "azure-keyvault" - -[packaging] -auto_update = false diff --git a/sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml b/sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml new file mode 100644 index 000000000000..e7687fdae93b --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/sdk_packaging.toml @@ -0,0 +1,2 @@ +[packaging] +auto_update = false \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py b/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py index 6663690fc1ca..4cc9668b3b8b 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py @@ -12,6 +12,7 @@ from azure.keyvault.keys._shared.client_base import ApiVersion from devtools_testutils import AzureRecordedTestCase + HSM_UNSUPPORTED_VERSIONS = {ApiVersion.V2016_10_01, ApiVersion.V7_0, ApiVersion.V7_1} diff --git a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py index 89a32415e45a..5f987a254b83 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/decrypt.py @@ -13,6 +13,7 @@ from azure.keyvault.keys.crypto.aio import CryptographyClient as AsyncCryptographyClient from azure.mgmt.keyvault.models import KeyPermissions, Permissions + # without keys/get, a CryptographyClient created with a key ID performs all ops remotely NO_GET = Permissions(keys=[p.value for p in KeyPermissions if p.value != "get"]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py index 36676296424d..469de42d8a35 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/sign.py @@ -14,6 +14,7 @@ from azure.keyvault.keys.crypto.aio import CryptographyClient as AsyncCryptographyClient from azure.mgmt.keyvault.models import KeyPermissions, Permissions + # without keys/get, a CryptographyClient created with a key ID performs all ops remotely NO_GET = Permissions(keys=[p.value for p in KeyPermissions if p.value != "get"]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py index 9df7fc252c22..2b09561f154b 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/perfstress_tests/unwrap.py @@ -13,6 +13,7 @@ from azure.keyvault.keys.crypto.aio import CryptographyClient as AsyncCryptographyClient from azure.mgmt.keyvault.models import KeyPermissions, Permissions + # without keys/get, a CryptographyClient created with a key ID performs all ops remotely NO_GET = Permissions(keys=[p.value for p in KeyPermissions if p.value != "get"]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py index f5a680f27674..5c32754a86ee 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py @@ -6,7 +6,6 @@ Tests for the HTTP challenge authentication implementation. These tests aren't parallelizable, because the challenge cache is global to the process. """ - import base64 import functools from itertools import product diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py index d70f2f9fc7c4..ab1e6af8c3d6 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py @@ -6,7 +6,6 @@ Tests for the HTTP challenge authentication implementation. These tests aren't parallelizable, because the challenge cache is global to the process. """ - import asyncio import functools from itertools import product diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py index 86f900f9f61a..382bee35dfb7 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -46,6 +46,7 @@ from _test_case import KeysClientPreparer, get_decorator from _keys_test_case import KeysTestCase + all_api_versions = get_decorator() only_hsm = get_decorator(only_hsm=True) only_vault_default = get_decorator(only_vault=True, api_versions=[DEFAULT_VERSION]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py index c2b665882c82..d101f0507fe1 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py @@ -32,6 +32,7 @@ from _shared.test_case_async import KeyVaultTestCase from _keys_test_case import KeysTestCase + all_api_versions = get_decorator(is_async=True) only_hsm = get_decorator(only_hsm=True, is_async=True) only_vault_7_4_plus = get_decorator(only_vault=True, is_async=True, api_versions=[ApiVersion.V7_4, ApiVersion.V7_5]) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py index 71866fef987e..fe21f207819b 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py @@ -35,6 +35,7 @@ from devtools_testutils.aio import recorded_by_proxy_async from _keys_test_case import KeysTestCase + all_api_versions = get_decorator(is_async=True) only_hsm = get_decorator(only_hsm=True, is_async=True) only_hsm_default = get_decorator(only_hsm=True, is_async=True, api_versions=[DEFAULT_VERSION]) From b68e0daeddb8302f461ebae01f7a4f9dfca31433 Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Wed, 20 May 2026 12:40:19 -0700 Subject: [PATCH 3/6] Update default API version to 2026-03-01-preview Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure/keyvault/keys/_shared/client_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py index 9e721ef6cfe2..c1da600e7011 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/client_base.py @@ -24,6 +24,7 @@ class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): """Key Vault API versions supported by this package""" #: this is the default version + V2026_03_01_PREVIEW = "2026-03-01-preview" V2025_07_01 = "2025-07-01" V7_6 = "7.6" V7_5 = "7.5" @@ -35,7 +36,7 @@ class ApiVersion(str, Enum, metaclass=CaseInsensitiveEnumMeta): V2016_10_01 = "2016-10-01" -DEFAULT_VERSION = ApiVersion.V2025_07_01 +DEFAULT_VERSION = ApiVersion.V2026_03_01_PREVIEW _SERIALIZER = Serializer() _SERIALIZER.client_side_validation = False From a4525f2c7448353a51627a8ab08a7b088f334454 Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Wed, 20 May 2026 13:22:59 -0700 Subject: [PATCH 4/6] Fix test imports: _serialization moved to _utils/serialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py | 2 +- .../azure-keyvault-keys/tests/test_crypto_client_async.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py index 382bee35dfb7..c2e75eda7619 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -37,7 +37,7 @@ SignatureAlgorithm, ) from azure.keyvault.keys.crypto._providers import NoLocalCryptography, get_local_cryptography_provider -from azure.keyvault.keys._generated._serialization import Deserializer +from azure.keyvault.keys._generated._utils.serialization import Deserializer from azure.keyvault.keys._generated.models import KeySignParameters from azure.keyvault.keys._shared.client_base import DEFAULT_VERSION from devtools_testutils import recorded_by_proxy, set_bodiless_matcher diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py index d101f0507fe1..cfc3e63c0c7f 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client_async.py @@ -22,7 +22,7 @@ KeyWrapAlgorithm, SignatureAlgorithm, ) -from azure.keyvault.keys._generated._serialization import Deserializer +from azure.keyvault.keys._generated._utils.serialization import Deserializer from azure.keyvault.keys._generated.models import KeySignParameters from devtools_testutils.aio import recorded_by_proxy_async From 192b39499d05bd2d742a85e262501a7519acd161 Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Thu, 21 May 2026 18:05:22 -0700 Subject: [PATCH 5/6] Update test recordings for 2026-03-01-preview API version Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk/keyvault/azure-keyvault-keys/assets.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index ad02b6cc8fcc..e929774db79c 100644 --- a/sdk/keyvault/azure-keyvault-keys/assets.json +++ b/sdk/keyvault/azure-keyvault-keys/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/keyvault/azure-keyvault-keys", - "Tag": "python/keyvault/azure-keyvault-keys_229759aadf" -} + "Tag": "python/keyvault/azure-keyvault-keys_a59f4659da" +} \ No newline at end of file From 541f070b3a9f53d60b333d8bebf8eb63716db5df Mon Sep 17 00:00:00 2001 From: Nicholas Noboa Date: Tue, 26 May 2026 14:42:53 -0700 Subject: [PATCH 6/6] Update recordings tag to _533efe50f4 (2026-03-01-preview + main baseline) Rebased recordings on main's _e47dc10e22 baseline (added by PR #47116) and layered our 2026-03-01-preview recordings on top. New tag contains: - 131 [2026-01-01-preview_*] files (from main's baseline) - 131 [2026-03-01-preview_*] files (ours, including 9 synthesized for tests new in main) Full-suite playback: 1464 passed, 88 skipped, 0 failed. --- sdk/keyvault/azure-keyvault-keys/assets.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index e929774db79c..d5d544ea3d61 100644 --- a/sdk/keyvault/azure-keyvault-keys/assets.json +++ b/sdk/keyvault/azure-keyvault-keys/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/keyvault/azure-keyvault-keys", - "Tag": "python/keyvault/azure-keyvault-keys_a59f4659da" -} \ No newline at end of file + "Tag": "python/keyvault/azure-keyvault-keys_533efe50f4" +}