From 5c789c61d85d04bdf010464b5de5c457de543d36 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 7 Sep 2023 19:13:29 -0700 Subject: [PATCH 01/29] Initial implementation --- .../azure/keyvault/keys/crypto/_models.py | 152 ++++++++++++++++-- 1 file changed, 141 insertions(+), 11 deletions(-) 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 index a1b9fb01aa90..43871e850c6a 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -2,11 +2,143 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from typing import TYPE_CHECKING +from typing import cast, Optional, Union, TYPE_CHECKING + +from cryptography.hazmat.primitives import _serialization, hashes +from cryptography.hazmat.primitives.asymmetric import utils as asym_utils +from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS +from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, + RSAPrivateNumbers, + RSAPublicKey, + RSAPublicNumbers, +) + +from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm if TYPE_CHECKING: - from . import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm - from typing import Any, Optional + from ._client import CryptographyClient + from .._client import KeyClient + + +SIGN_ALGORITHM_MAP = { + hashes.SHA256: SignatureAlgorithm.rs256, + hashes.SHA384: SignatureAlgorithm.rs384, + hashes.SHA512: SignatureAlgorithm.rs512, +} +PSS_MAP = { + SignatureAlgorithm.rs256: SignatureAlgorithm.ps256, + SignatureAlgorithm.rs384: SignatureAlgorithm.ps384, + SignatureAlgorithm.rs512: SignatureAlgorithm.ps512, +} + + +class KeyVaultPrivateKey(RSAPrivateKey): + """An `RSAPrivateKey` implementation based on a key managed by Key Vault. + + :param str key_name: The name of the key vault key to use. + :param key_client: The client that will be used to communicate with Key Vault. + :type key_client: KeyClient + """ + + def __init__(self, key_name: str, key_client: "KeyClient", **kwargs) -> None: # pylint:disable=unused-argument + self._key_name: str = key_name + self._key_client: "KeyClient" = key_client + self._crypto_client: "CryptographyClient" = key_client.get_cryptography_client(key_name) + + 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, `SHA256` + will be used as the encryption algorithm and MGF1 will be used as the mask generation function. + See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + + :returns: The decrypted plaintext, as bytes. + :rtype: bytes + """ + if isinstance(padding, OAEP): + # There's no public algorithm attribute attached to an OAEP padding instance, so we default to SHA256 + algorithm = EncryptionAlgorithm.rsa_oaep_256 + if isinstance(padding, PKCS1v15): + algorithm = EncryptionAlgorithm.rsa1_5 + else: + raise ValueError(f"Unsupported padding: {padding.name}") + result = self._crypto_client.decrypt(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 + """ + key = self._key_client.get_key(self._key_name) + return int.from_bytes(key.key.n, "big").bit_length() # type: ignore[attr-defined] + + def public_key(self) -> RSAPublicKey: + """The `RSAPublicKey` associated with this private key. + + :returns: The `RSAPublicKey` associated with the key. + :rtype: RSAPublicKey + """ + key = self._key_client.get_key(self._key_name) + e = int.from_bytes(key.key.e, "big") # type: ignore[attr-defined] + n = int.from_bytes(key.key.n, "big") # type: ignore[attr-defined] + public_numbers = RSAPublicNumbers(e, n) + return public_numbers.public_key() + + def sign( + self, + data: bytes, + padding: AsymmetricPadding, + algorithm: Union[asym_utils.Prehashed, hashes.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`. If `PSS` is given, the + operation will use RSASSA-PSS using SHA-x and MGF1 with SHA-x, where "x" is determined by the `algorithm` + provided. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + :type padding: AsymmetricPadding + :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, + `SHA384`, and `SHA512`. + :type algorithm: asym_utils.Prehashed or hashes.HashAlgorithm + + :returns: The signature, as bytes. + :rtype: bytes + """ + if isinstance(algorithm, asym_utils.Prehashed): + raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") + + 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) + # The only other padding accepted is PKCS1v15 + elif not isinstance(padding, PKCS1v15): + raise ValueError(f"Unsupported padding: {padding.name}") + + digest = hashes.Hash(algorithm) + digest.update(data) + result = self._crypto_client.sign(cast(SignatureAlgorithm, mapped_algorithm), digest.finalize()) + return result.signature + + def private_numbers(self) -> RSAPrivateNumbers: + """Returns an RSAPrivateNumbers. Not implemented, as the private key is managed by Key Vault.""" + raise NotImplementedError() + + def private_bytes( + self, + encoding: _serialization.Encoding, + format: _serialization.PrivateFormat, + encryption_algorithm: _serialization.KeySerializationEncryption, + ) -> bytes: + """Returns the key serialized as bytes. Not implemented, as the private key is managed by Key Vault.""" + raise NotImplementedError() class DecryptResult: @@ -18,7 +150,7 @@ class DecryptResult: :param bytes plaintext: The decrypted bytes """ - def __init__(self, key_id: "Optional[str]", algorithm: "EncryptionAlgorithm", plaintext: bytes) -> None: + def __init__(self, key_id: Optional[str], algorithm: EncryptionAlgorithm, plaintext: bytes) -> None: self.key_id = key_id self.algorithm = algorithm self.plaintext = plaintext @@ -39,9 +171,7 @@ class EncryptResult: authenticated algorithm """ - def __init__( - self, key_id: "Optional[str]", algorithm: "EncryptionAlgorithm", ciphertext: bytes, **kwargs - ) -> None: + def __init__(self, key_id: Optional[str], algorithm: EncryptionAlgorithm, ciphertext: bytes, **kwargs) -> None: self.key_id = key_id self.algorithm = algorithm self.ciphertext = ciphertext @@ -59,7 +189,7 @@ class SignResult: :param bytes signature: """ - def __init__(self, key_id: "Optional[str]", algorithm: "SignatureAlgorithm", signature: bytes) -> None: + def __init__(self, key_id: Optional[str], algorithm: SignatureAlgorithm, signature: bytes) -> None: self.key_id = key_id self.algorithm = algorithm self.signature = signature @@ -74,7 +204,7 @@ class VerifyResult: :type algorithm: ~azure.keyvault.keys.crypto.SignatureAlgorithm """ - def __init__(self, key_id: "Optional[str]", is_valid: bool, algorithm: "SignatureAlgorithm") -> None: + 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 @@ -89,7 +219,7 @@ class UnwrapResult: :param bytes key: The unwrapped key """ - def __init__(self, key_id: "Optional[str]", algorithm: "KeyWrapAlgorithm", key: bytes) -> None: + def __init__(self, key_id: Optional[str], algorithm: KeyWrapAlgorithm, key: bytes) -> None: self.key_id = key_id self.algorithm = algorithm self.key = key @@ -104,7 +234,7 @@ class WrapResult: :param bytes encrypted_key: The encrypted key bytes """ - def __init__(self, key_id: "Optional[str]", algorithm: "KeyWrapAlgorithm", encrypted_key: bytes) -> None: + 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 From 18b742a1e8861e507528c3fcc77caae2de238594 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Fri, 8 Sep 2023 13:40:07 -0700 Subject: [PATCH 02/29] cspell --- .vscode/cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 6970bee9583e..09e89d00293b 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -825,7 +825,8 @@ { "filename": "sdk/keyvault/**", "words": [ - "eddsa" + "eddsa", + "asym" ] }, { From 56cf5dea4cdf783d9bd4ff6aa641630a0e5e8fd7 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Fri, 8 Sep 2023 13:42:37 -0700 Subject: [PATCH 03/29] pylint --- .../azure/keyvault/keys/crypto/_models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 43871e850c6a..54fe623a7733 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -53,6 +53,7 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: :param padding: The padding to use. Supported paddings are `OAEP` and `PKCS1v15`. For `OAEP` padding, `SHA256` will be used as the encryption algorithm and MGF1 will be used as the mask generation function. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + :type padding: AsymmetricPadding :returns: The decrypted plaintext, as bytes. :rtype: bytes @@ -127,11 +128,11 @@ def sign( result = self._crypto_client.sign(cast(SignatureAlgorithm, mapped_algorithm), digest.finalize()) return result.signature - def private_numbers(self) -> RSAPrivateNumbers: + def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-missing-return,docstring-missing-rtype """Returns an RSAPrivateNumbers. Not implemented, as the private key is managed by Key Vault.""" raise NotImplementedError() - def private_bytes( + def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype self, encoding: _serialization.Encoding, format: _serialization.PrivateFormat, From ccc368621034e4c1d744bf9b8105c8b488f82154 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Fri, 8 Sep 2023 13:44:44 -0700 Subject: [PATCH 04/29] Update import source --- .../azure/keyvault/keys/crypto/_models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 54fe623a7733..0ce2d882537d 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -4,7 +4,7 @@ # ------------------------------------ from typing import cast, Optional, Union, TYPE_CHECKING -from cryptography.hazmat.primitives import _serialization, hashes +from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS from cryptography.hazmat.primitives.asymmetric.rsa import ( @@ -134,9 +134,9 @@ def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-miss def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype self, - encoding: _serialization.Encoding, - format: _serialization.PrivateFormat, - encryption_algorithm: _serialization.KeySerializationEncryption, + encoding: serialization.Encoding, + format: serialization.PrivateFormat, + encryption_algorithm: serialization.KeySerializationEncryption, ) -> bytes: """Returns the key serialized as bytes. Not implemented, as the private key is managed by Key Vault.""" raise NotImplementedError() From 96c04a88426f766a5748a99da8fdce2886b4d5c7 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 11 Sep 2023 16:48:55 -0700 Subject: [PATCH 05/29] Remove kwargs --- .../azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 0ce2d882537d..78fc0d1191c5 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -33,7 +33,7 @@ } -class KeyVaultPrivateKey(RSAPrivateKey): +class KeyVaultManagedKey(RSAPrivateKey): """An `RSAPrivateKey` implementation based on a key managed by Key Vault. :param str key_name: The name of the key vault key to use. @@ -41,7 +41,7 @@ class KeyVaultPrivateKey(RSAPrivateKey): :type key_client: KeyClient """ - def __init__(self, key_name: str, key_client: "KeyClient", **kwargs) -> None: # pylint:disable=unused-argument + def __init__(self, key_name: str, key_client: "KeyClient") -> None: # pylint:disable=unused-argument self._key_name: str = key_name self._key_client: "KeyClient" = key_client self._crypto_client: "CryptographyClient" = key_client.get_cryptography_client(key_name) From af15d76bee4b6e47d2918d747be7b67e7ba0f567 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 11 Sep 2023 18:20:10 -0700 Subject: [PATCH 06/29] Integrate public alg/mgf properties --- .../azure/keyvault/keys/crypto/_models.py | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) 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 index 78fc0d1191c5..720716c2f7b2 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -6,7 +6,7 @@ from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import utils as asym_utils -from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS +from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS, MGF1 from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPrivateKey, RSAPrivateNumbers, @@ -17,6 +17,7 @@ from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm if TYPE_CHECKING: + # Import clients only during TYPE_CHECKING to avoid circular dependency from ._client import CryptographyClient from .._client import KeyClient @@ -26,6 +27,10 @@ hashes.SHA384: SignatureAlgorithm.rs384, hashes.SHA512: SignatureAlgorithm.rs512, } +OAEP_MAP = { + hashes.SHA1: EncryptionAlgorithm.rsa_oaep, + hashes.SHA256: EncryptionAlgorithm.rsa_oaep_256 +} PSS_MAP = { SignatureAlgorithm.rs256: SignatureAlgorithm.ps256, SignatureAlgorithm.rs384: SignatureAlgorithm.ps384, @@ -50,17 +55,31 @@ 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, `SHA256` - will be used as the encryption algorithm and MGF1 will be used as the mask generation function. - See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + :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: AsymmetricPadding :returns: The decrypted plaintext, as bytes. :rtype: bytes """ if isinstance(padding, OAEP): - # There's no public algorithm attribute attached to an OAEP padding instance, so we default to SHA256 - algorithm = EncryptionAlgorithm.rsa_oaep_256 + # 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 = OAEP_MAP.get(padding.algorithm) + except AttributeError: + algorithm = OAEP_MAP.get(padding._algorithm) + if 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 + except AttributeError: + mgf = padding._mgf + if not isinstance(mgf, MGF1): + raise ValueError(f"Unsupported MGF: {mgf}") if isinstance(padding, PKCS1v15): algorithm = EncryptionAlgorithm.rsa1_5 else: @@ -99,9 +118,9 @@ def sign( """Signs the data. :param bytes data: The data to sign, as bytes. - :param padding: The padding to use. Supported paddings are `PKCS1v15` and `PSS`. If `PSS` is given, the - operation will use RSASSA-PSS using SHA-x and MGF1 with SHA-x, where "x" is determined by the `algorithm` - provided. See https://learn.microsoft.com/azure/key-vault/keys/about-keys-details for details. + :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: AsymmetricPadding :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, `SHA384`, and `SHA512`. @@ -116,9 +135,20 @@ def sign( 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 + except AttributeError: + mgf = padding._mgf + 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}") From f2eaee6b3de43d8cbefc787e15701160784cbd73 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 11 Sep 2023 18:39:14 -0700 Subject: [PATCH 07/29] Update class name, imports --- .../azure/keyvault/keys/crypto/_models.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) 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 index 720716c2f7b2..6996d1f224b5 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -4,8 +4,6 @@ # ------------------------------------ from typing import cast, Optional, Union, TYPE_CHECKING -from cryptography.hazmat.primitives import serialization, hashes -from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, OAEP, PKCS1v15, PSS, MGF1 from cryptography.hazmat.primitives.asymmetric.rsa import ( RSAPrivateKey, @@ -13,6 +11,9 @@ 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 from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm @@ -23,13 +24,13 @@ SIGN_ALGORITHM_MAP = { - hashes.SHA256: SignatureAlgorithm.rs256, - hashes.SHA384: SignatureAlgorithm.rs384, - hashes.SHA512: SignatureAlgorithm.rs512, + SHA256: SignatureAlgorithm.rs256, + SHA384: SignatureAlgorithm.rs384, + SHA512: SignatureAlgorithm.rs512, } OAEP_MAP = { - hashes.SHA1: EncryptionAlgorithm.rsa_oaep, - hashes.SHA256: EncryptionAlgorithm.rsa_oaep_256 + SHA1: EncryptionAlgorithm.rsa_oaep, + SHA256: EncryptionAlgorithm.rsa_oaep_256 } PSS_MAP = { SignatureAlgorithm.rs256: SignatureAlgorithm.ps256, @@ -38,7 +39,7 @@ } -class KeyVaultManagedKey(RSAPrivateKey): +class ManagedRsaKey(RSAPrivateKey): """An `RSAPrivateKey` implementation based on a key managed by Key Vault. :param str key_name: The name of the key vault key to use. @@ -113,7 +114,7 @@ def sign( self, data: bytes, padding: AsymmetricPadding, - algorithm: Union[asym_utils.Prehashed, hashes.HashAlgorithm], + algorithm: Union[Prehashed, HashAlgorithm], ) -> bytes: """Signs the data. @@ -124,12 +125,12 @@ def sign( :type padding: AsymmetricPadding :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, `SHA384`, and `SHA512`. - :type algorithm: asym_utils.Prehashed or hashes.HashAlgorithm + :type algorithm: Prehashed or HashAlgorithm :returns: The signature, as bytes. :rtype: bytes """ - if isinstance(algorithm, asym_utils.Prehashed): + if isinstance(algorithm, Prehashed): raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") mapped_algorithm = SIGN_ALGORITHM_MAP.get(type(algorithm)) @@ -153,7 +154,7 @@ def sign( elif not isinstance(padding, PKCS1v15): raise ValueError(f"Unsupported padding: {padding.name}") - digest = hashes.Hash(algorithm) + digest = Hash(algorithm) digest.update(data) result = self._crypto_client.sign(cast(SignatureAlgorithm, mapped_algorithm), digest.finalize()) return result.signature @@ -164,9 +165,9 @@ def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-miss def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype self, - encoding: serialization.Encoding, - format: serialization.PrivateFormat, - encryption_algorithm: serialization.KeySerializationEncryption, + encoding: Encoding, + format: PrivateFormat, + encryption_algorithm: KeySerializationEncryption, ) -> bytes: """Returns the key serialized as bytes. Not implemented, as the private key is managed by Key Vault.""" raise NotImplementedError() From 1b32b3d2ed5f1df0acc8ad3105db613b8c7fd664 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 11 Sep 2023 19:10:59 -0700 Subject: [PATCH 08/29] Add decryption test --- sdk/keyvault/azure-keyvault-keys/assets.json | 2 +- .../azure/keyvault/keys/crypto/__init__.py | 3 +- .../azure/keyvault/keys/crypto/_models.py | 15 ++++----- .../tests/test_crypto_client.py | 31 ++++++++++++++++--- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index 662d84e7e5da..ebe6a9f07daa 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_a5a3c2671c" + "Tag": "python/keyvault/azure-keyvault-keys_95708a33a7" } 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 index 44006de7365e..4b2a3a3aca19 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py @@ -2,7 +2,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from ._models import DecryptResult, EncryptResult, SignResult, WrapResult, VerifyResult, UnwrapResult +from ._models import DecryptResult, EncryptResult, ManagedRsaKey, SignResult, WrapResult, VerifyResult, UnwrapResult from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm from ._client import CryptographyClient @@ -13,6 +13,7 @@ "EncryptionAlgorithm", "EncryptResult", "KeyWrapAlgorithm", + "ManagedRsaKey", "SignatureAlgorithm", "SignResult", "WrapResult", 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 index 6996d1f224b5..36e2d2e6259b 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -68,10 +68,11 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: # 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 = OAEP_MAP.get(padding.algorithm) + algorithm = padding.algorithm except AttributeError: - algorithm = OAEP_MAP.get(padding._algorithm) - if algorithm is None: + algorithm = padding._algorithm + 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 @@ -81,11 +82,11 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: mgf = padding._mgf if not isinstance(mgf, MGF1): raise ValueError(f"Unsupported MGF: {mgf}") - if isinstance(padding, PKCS1v15): - algorithm = EncryptionAlgorithm.rsa1_5 + elif isinstance(padding, PKCS1v15): + mapped_algorithm = EncryptionAlgorithm.rsa1_5 else: raise ValueError(f"Unsupported padding: {padding.name}") - result = self._crypto_client.decrypt(algorithm, ciphertext) + result = self._crypto_client.decrypt(mapped_algorithm, ciphertext) return result.plaintext @property @@ -139,7 +140,7 @@ def sign( # If PSS padding is requested, use the PSS equivalent algorithm if isinstance(padding, PSS): - mapped_algorithm = PSS_MAP.get(mapped_algorithm) + mapped_algorithm = PSS_MAP.get(type(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 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 b3d617c0905c..1e2e8d215cb4 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -7,14 +7,12 @@ import os import time from datetime import datetime, timezone +from unittest import mock from devtools_testutils import recorded_by_proxy, set_bodiless_matcher -try: - from unittest import mock -except ImportError: - import mock - +from cryptography.hazmat.primitives.hashes import SHA1 +from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP import pytest from azure.core.exceptions import AzureError, HttpResponseError from azure.core.pipeline.policies import SansIOHTTPPolicy @@ -24,6 +22,7 @@ CryptographyClient, EncryptionAlgorithm, KeyWrapAlgorithm, + ManagedRsaKey, SignatureAlgorithm, ) from azure.keyvault.keys.crypto._providers import NoLocalCryptography, get_local_cryptography_provider @@ -194,6 +193,28 @@ def test_encrypt_and_decrypt(self, key_client, is_hsm, **kwargs): assert EncryptionAlgorithm.rsa_oaep == result.algorithm assert self.plaintext == result.plaintext + @pytest.mark.parametrize("api_version,is_hsm", only_vault_latest) + @KeysClientPreparer() + @recorded_by_proxy + def test_encrypt_and_decrypt_with_managed_key(self, key_client, **kwargs): + set_bodiless_matcher() + key_name = self.get_resource_name("keycrypt") + + imported_key = self._import_test_key(key_client, key_name) + crypto_client = self.create_crypto_client(imported_key.id, api_version=key_client.api_version) + + result = crypto_client.encrypt(EncryptionAlgorithm.rsa_oaep, self.plaintext) + assert result.key_id == imported_key.id + + # Create a ManagedRsaKey that can perform decryption with `cryptography`'s interface + managed_key = ManagedRsaKey(key_name=key_name, key_client=key_client) + # We used RSA-OAEP to encrypt, so we use MGF1 and SHA1 as inputs to OAEP to match during decryption + algorithm = SHA1() + mgf = MGF1(algorithm) + padding = OAEP(mgf, algorithm, None) + plaintext = managed_key.decrypt(ciphertext=result.ciphertext, padding=padding) + assert self.plaintext == plaintext + @pytest.mark.parametrize("api_version,is_hsm", no_get) @KeysClientPreparer(permissions=NO_GET) @recorded_by_proxy From cff56f6bf4230b19d0e1e857bcf617f65dc682b3 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Tue, 12 Sep 2023 03:47:35 -0700 Subject: [PATCH 09/29] Clean up local crypto tests --- .../tests/test_crypto_client.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 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 1e2e8d215cb4..2a5ad6550629 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -776,8 +776,29 @@ def test_local_only_mode_no_service_calls(): def test_local_only_mode_raise(): """A local-only CryptographyClient should raise an exception if an operation can't be performed locally""" - - jwk = {"kty":"RSA", "key_ops":["decrypt", "verify", "unwrapKey"], "n":b"10011", "e":b"10001"} + def _to_bytes(hex): + if len(hex) % 2: + hex = f"0{hex}" + return codecs.decode(hex, "hex_codec") + + # Create an RSA key with private components so that the JWK could theoretically be used for private operations + jwk = { + "kty":"RSA", + "key_ops":["decrypt", "verify", "unwrapKey"], + "n":_to_bytes( + "00a0914d00234ac683b21b4c15d5bed887bdc959c2e57af54ae734e8f00720d775d275e455207e3784ceeb60a50a4655dd72a7a94d271e8ee8f7959a669ca6e775bf0e23badae991b4529d978528b4bd90521d32dd2656796ba82b6bbfc7668c8f5eeb5053747fd199319d29a8440d08f4412d527ff9311eda71825920b47b1c46b11ab3e91d7316407e89c7f340f7b85a34042ce51743b27d4718403d34c7b438af6181be05e4d11eb985d38253d7fe9bf53fc2f1b002d22d2d793fa79a504b6ab42d0492804d7071d727a06cf3a8893aa542b1503f832b296371b6707d4dc6e372f8fe67d8ded1c908fde45ce03bc086a71487fa75e43aa0e0679aa0d20efe35" + ), + "e":_to_bytes("10001"), + "p":_to_bytes( + "00d1deac8d68ddd2c1fd52d5999655b2cf1565260de5269e43fd2a85f39280e1708ffff0682166cb6106ee5ea5e9ffd9f98d0becc9ff2cda2febc97259215ad84b9051e563e14a051dce438bc6541a24ac4f014cf9732d36ebfc1e61a00d82cbe412090f7793cfbd4b7605be133dfc3991f7e1bed5786f337de5036fc1e2df4cf3" + ), + "q":_to_bytes( + "00c3dc66b641a9b73cd833bc439cd34fc6574465ab5b7e8a92d32595a224d56d911e74624225b48c15a670282a51c40d1dad4bc2e9a3c8dab0c76f10052dfb053bc6ed42c65288a8e8bace7a8881184323f94d7db17ea6dfba651218f931a93b8f738f3d8fd3f6ba218d35b96861a0f584b0ab88ddcf446b9815f4d287d83a3237" + ), + "d":_to_bytes( + "627c7d24668148fe2252c7fa649ea8a5a9ed44d75c766cda42b29b660e99404f0e862d4561a6c95af6a83d213e0a2244b03cd28576473215073785fb067f015da19084ade9f475e08b040a9a2c7ba00253bb8125508c9df140b75161d266be347a5e0f6900fe1d8bbf78ccc25eeb37e0c9d188d6e1fc15169ba4fe12276193d77790d2326928bd60d0d01d6ead8d6ac4861abadceec95358fd6689c50a1671a4a936d2376440a41445501da4e74bfb98f823bd19c45b94eb01d98fc0d2f284507f018ebd929b8180dbe6381fdd434bffb7800aaabdd973d55f9eaf9bb88a6ea7b28c2a80231e72de1ad244826d665582c2362761019de2e9f10cb8bcc2625649" + ) + } client = CryptographyClient.from_jwk(jwk=jwk) # Algorithm not supported locally @@ -799,9 +820,8 @@ def test_local_only_mode_raise(): assert f"{KeyOperation.verify}" in str(ex.value) # Algorithm not supported locally, and operation not included in JWK permissions - with pytest.raises(NotImplementedError) as ex: + with pytest.raises(AzureError) as ex: client.sign(SignatureAlgorithm.rs256, b"...") - assert f"{SignatureAlgorithm.rs256}" in str(ex.value) assert f"{KeyOperation.sign}" in str(ex.value) # Algorithm not supported locally From feb27f7aedf332fc278cd9754e0676e47b4b2808 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Tue, 12 Sep 2023 03:54:34 -0700 Subject: [PATCH 10/29] Rename key type --- .../azure/keyvault/keys/crypto/__init__.py | 12 ++++++++++-- .../azure/keyvault/keys/crypto/_models.py | 2 +- .../azure-keyvault-keys/tests/test_crypto_client.py | 6 +++--- 3 files changed, 14 insertions(+), 6 deletions(-) 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 index 4b2a3a3aca19..95629c2daa72 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py @@ -2,7 +2,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from ._models import DecryptResult, EncryptResult, ManagedRsaKey, SignResult, WrapResult, VerifyResult, UnwrapResult +from ._models import ( + DecryptResult, + EncryptResult, + KeyVaultRSAPrivateKey, + SignResult, + WrapResult, + VerifyResult, + UnwrapResult, +) from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm from ._client import CryptographyClient @@ -12,8 +20,8 @@ "DecryptResult", "EncryptionAlgorithm", "EncryptResult", + "KeyVaultRSAPrivateKey", "KeyWrapAlgorithm", - "ManagedRsaKey", "SignatureAlgorithm", "SignResult", "WrapResult", 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 index 36e2d2e6259b..ab20e7204899 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -39,7 +39,7 @@ } -class ManagedRsaKey(RSAPrivateKey): +class KeyVaultRSAPrivateKey(RSAPrivateKey): """An `RSAPrivateKey` implementation based on a key managed by Key Vault. :param str key_name: The name of the key vault key to use. 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 2a5ad6550629..5d370c9bf22d 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -21,8 +21,8 @@ from azure.keyvault.keys.crypto import ( CryptographyClient, EncryptionAlgorithm, + KeyVaultRSAPrivateKey, KeyWrapAlgorithm, - ManagedRsaKey, SignatureAlgorithm, ) from azure.keyvault.keys.crypto._providers import NoLocalCryptography, get_local_cryptography_provider @@ -206,8 +206,8 @@ def test_encrypt_and_decrypt_with_managed_key(self, key_client, **kwargs): result = crypto_client.encrypt(EncryptionAlgorithm.rsa_oaep, self.plaintext) assert result.key_id == imported_key.id - # Create a ManagedRsaKey that can perform decryption with `cryptography`'s interface - managed_key = ManagedRsaKey(key_name=key_name, key_client=key_client) + # Create a KeyVaultRSAPrivateKey that can perform decryption with `cryptography`'s interface + managed_key = KeyVaultRSAPrivateKey(key_name=key_name, key_client=key_client) # We used RSA-OAEP to encrypt, so we use MGF1 and SHA1 as inputs to OAEP to match during decryption algorithm = SHA1() mgf = MGF1(algorithm) From 492eb3ad181dd071a0991e66629d9c969b5e7950 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 12:07:39 -0700 Subject: [PATCH 11/29] Create key with CryptographyClient method --- .../azure/keyvault/keys/crypto/_client.py | 10 +++++ .../azure/keyvault/keys/crypto/_models.py | 38 ++++++++++--------- 2 files changed, 30 insertions(+), 18 deletions(-) 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 index 03a02539ca57..6708d0b8bbef 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py @@ -10,6 +10,7 @@ from . import DecryptResult, EncryptionAlgorithm, EncryptResult, SignResult, VerifyResult, UnwrapResult, WrapResult from ._key_validity import raise_if_time_invalid +from ._models import KeyVaultRSAPrivateKey from ._providers import get_local_cryptography_provider, NoLocalCryptography from .. import KeyOperation from .._models import JsonWebKey, KeyVaultKey @@ -211,6 +212,15 @@ def _initialize(self, **kwargs) -> None: # 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: + """Create an `RSAPrivateKey` implementation backed by this `CryptographyClient` and key. + + The `CryptographyClient` will attempt to download the key, if it hasn't been already, as part of this operation. + """ + self._initialize() + return KeyVaultRSAPrivateKey(client=self, key_id=self.key_id, key_material=cast(JsonWebKey, self._key)) + @distributed_trace def encrypt(self, algorithm: "EncryptionAlgorithm", plaintext: bytes, **kwargs) -> EncryptResult: """Encrypt bytes using the client's key. 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 index ab20e7204899..aa958795ff88 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -16,11 +16,12 @@ from cryptography.hazmat.primitives.serialization import Encoding, KeySerializationEncryption, PrivateFormat from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm +from .._models import JsonWebKey +from .._shared import KeyVaultResourceId, parse_key_vault_id if TYPE_CHECKING: - # Import clients only during TYPE_CHECKING to avoid circular dependency + # Import client only during TYPE_CHECKING to avoid circular dependency from ._client import CryptographyClient - from .._client import KeyClient SIGN_ALGORITHM_MAP = { @@ -40,17 +41,20 @@ class KeyVaultRSAPrivateKey(RSAPrivateKey): - """An `RSAPrivateKey` implementation based on a key managed by Key Vault. + """An `RSAPrivateKey` implementation based on a key managed by Key Vault.""" - :param str key_name: The name of the key vault key to use. - :param key_client: The client that will be used to communicate with Key Vault. - :type key_client: KeyClient - """ + def __init__(self, client: "CryptographyClient", key_id: str, key_material: JsonWebKey) -> None: + """Creates a `KeyVaultRSAPrivateKey` from a `CryptographyClient` and key. - def __init__(self, key_name: str, key_client: "KeyClient") -> None: # pylint:disable=unused-argument - self._key_name: str = key_name - self._key_client: "KeyClient" = key_client - self._crypto_client: "CryptographyClient" = key_client.get_cryptography_client(key_name) + :param client: The client that will be used to communicate with Key Vault. + :type client: :class:`~azure.keyvault.keys.crypto.CryptographyClient` + :param str key_id: The full identifier of the Key Vault key. + :param key_material: They Key Vault key's material, as a `JsonWebKey`. + :type key_material: :class:`~azure.keyvault.keys.JsonWebKey` + """ + self._client: "CryptographyClient" = client + self._key_id: str = parse_key_vault_id(key_id) + self._key: JsonWebKey = key_material def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: """Decrypts the provided ciphertext. @@ -86,7 +90,7 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: mapped_algorithm = EncryptionAlgorithm.rsa1_5 else: raise ValueError(f"Unsupported padding: {padding.name}") - result = self._crypto_client.decrypt(mapped_algorithm, ciphertext) + result = self._client.decrypt(mapped_algorithm, ciphertext) return result.plaintext @property @@ -96,8 +100,7 @@ def key_size(self) -> int: :returns: The key's size. :rtype: int """ - key = self._key_client.get_key(self._key_name) - return int.from_bytes(key.key.n, "big").bit_length() # type: ignore[attr-defined] + return int.from_bytes(self._key.n, "big").bit_length() # type: ignore[attr-defined] def public_key(self) -> RSAPublicKey: """The `RSAPublicKey` associated with this private key. @@ -105,9 +108,8 @@ def public_key(self) -> RSAPublicKey: :returns: The `RSAPublicKey` associated with the key. :rtype: RSAPublicKey """ - key = self._key_client.get_key(self._key_name) - e = int.from_bytes(key.key.e, "big") # type: ignore[attr-defined] - n = int.from_bytes(key.key.n, "big") # type: ignore[attr-defined] + 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) return public_numbers.public_key() @@ -157,7 +159,7 @@ def sign( digest = Hash(algorithm) digest.update(data) - result = self._crypto_client.sign(cast(SignatureAlgorithm, mapped_algorithm), digest.finalize()) + result = self._client.sign(cast(SignatureAlgorithm, mapped_algorithm), digest.finalize()) return result.signature def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-missing-return,docstring-missing-rtype From fc9e24e1256d96471807ae27bf5bb24ee85b60a5 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 13:02:49 -0700 Subject: [PATCH 12/29] Change key_size impl --- .../azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index aa958795ff88..1f789d8c79bb 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -100,7 +100,7 @@ def key_size(self) -> int: :returns: The key's size. :rtype: int """ - return int.from_bytes(self._key.n, "big").bit_length() # type: ignore[attr-defined] + return len(self._key.n) * 8 # type: ignore[attr-defined] def public_key(self) -> RSAPublicKey: """The `RSAPublicKey` associated with this private key. From e60de2460ed1c080b8c073c3df3f07dba4169236 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 13:18:51 -0700 Subject: [PATCH 13/29] Implement private_numbers --- .../azure/keyvault/keys/crypto/_models.py | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) 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 index 1f789d8c79bb..40fd8bc519c4 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -2,10 +2,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -from typing import cast, Optional, Union, TYPE_CHECKING +from typing import cast, Optional, NoReturn, Union, TYPE_CHECKING 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, @@ -53,7 +57,7 @@ def __init__(self, client: "CryptographyClient", key_id: str, key_material: Json :type key_material: :class:`~azure.keyvault.keys.JsonWebKey` """ self._client: "CryptographyClient" = client - self._key_id: str = parse_key_vault_id(key_id) + self._key_id: KeyVaultResourceId = parse_key_vault_id(key_id) self._key: JsonWebKey = key_material def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: @@ -74,16 +78,16 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: try: algorithm = padding.algorithm except AttributeError: - algorithm = padding._algorithm + 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 except AttributeError: - mgf = padding._mgf + mgf = padding._mgf # pylint:disable=protected-access if not isinstance(mgf, MGF1): raise ValueError(f"Unsupported MGF: {mgf}") elif isinstance(padding, PKCS1v15): @@ -149,7 +153,7 @@ def sign( try: mgf = padding.mgf except AttributeError: - mgf = padding._mgf + mgf = padding._mgf # pylint:disable=protected-access if not isinstance(mgf, MGF1): raise ValueError(f"Unsupported MGF: {mgf}") @@ -163,15 +167,40 @@ def sign( return result.signature def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-missing-return,docstring-missing-rtype - """Returns an RSAPrivateNumbers. Not implemented, as the private key is managed by Key Vault.""" - raise NotImplementedError() + """Returns an `RSAPrivateNumbers`.""" + # 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 p is None or q is None: + if d is None: + raise ValueError("An 'RSAPrivateNumbers' couldn't be created with the available key material.") + 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( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype self, encoding: Encoding, format: PrivateFormat, encryption_algorithm: KeySerializationEncryption, - ) -> bytes: + ) -> NoReturn: """Returns the key serialized as bytes. Not implemented, as the private key is managed by Key Vault.""" raise NotImplementedError() From 05d43154d59887cf8ca229a61da38115bd4cd106 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 13:58:18 -0700 Subject: [PATCH 14/29] pylint; mypy; remove key_id param --- .../azure/keyvault/keys/crypto/_client.py | 4 ++-- .../azure/keyvault/keys/crypto/_models.py | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) 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 index 6708d0b8bbef..dc971e2d8cf5 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py @@ -214,12 +214,12 @@ def _initialize(self, **kwargs) -> None: @distributed_trace def create_rsa_private_key(self) -> KeyVaultRSAPrivateKey: - """Create an `RSAPrivateKey` implementation backed by this `CryptographyClient` and key. + """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. """ self._initialize() - return KeyVaultRSAPrivateKey(client=self, key_id=self.key_id, key_material=cast(JsonWebKey, self._key)) + return KeyVaultRSAPrivateKey(client=self, key_material=cast(JsonWebKey, self._key)) @distributed_trace def encrypt(self, algorithm: "EncryptionAlgorithm", plaintext: bytes, **kwargs) -> EncryptResult: 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 index 40fd8bc519c4..c84ac33a7a55 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -21,7 +21,6 @@ from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm from .._models import JsonWebKey -from .._shared import KeyVaultResourceId, parse_key_vault_id if TYPE_CHECKING: # Import client only during TYPE_CHECKING to avoid circular dependency @@ -47,17 +46,15 @@ class KeyVaultRSAPrivateKey(RSAPrivateKey): """An `RSAPrivateKey` implementation based on a key managed by Key Vault.""" - def __init__(self, client: "CryptographyClient", key_id: str, key_material: JsonWebKey) -> None: + def __init__(self, client: "CryptographyClient", key_material: JsonWebKey) -> None: """Creates a `KeyVaultRSAPrivateKey` from a `CryptographyClient` and key. :param client: The client that will be used to communicate with Key Vault. :type client: :class:`~azure.keyvault.keys.crypto.CryptographyClient` - :param str key_id: The full identifier of the Key Vault key. :param key_material: They Key Vault key's material, as a `JsonWebKey`. :type key_material: :class:`~azure.keyvault.keys.JsonWebKey` """ self._client: "CryptographyClient" = client - self._key_id: KeyVaultResourceId = parse_key_vault_id(key_id) self._key: JsonWebKey = key_material def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: @@ -76,7 +73,7 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: # 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 + algorithm = padding.algorithm # type: ignore[attr-defined] except AttributeError: algorithm = padding._algorithm # pylint:disable=protected-access mapped_algorithm = OAEP_MAP.get(type(algorithm)) @@ -85,7 +82,7 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: # Public mgf property was added at the same time as algorithm try: - mgf = padding.mgf + mgf = padding.mgf # type: ignore[attr-defined] except AttributeError: mgf = padding._mgf # pylint:disable=protected-access if not isinstance(mgf, MGF1): @@ -146,12 +143,12 @@ def sign( # If PSS padding is requested, use the PSS equivalent algorithm if isinstance(padding, PSS): - mapped_algorithm = PSS_MAP.get(type(mapped_algorithm)) + 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 + mgf = padding.mgf # type: ignore[attr-defined] except AttributeError: mgf = padding._mgf # pylint:disable=protected-access if not isinstance(mgf, MGF1): @@ -182,9 +179,9 @@ def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-miss 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: - if d is None: - raise ValueError("An 'RSAPrivateNumbers' couldn't be created with the available key material.") p, q = rsa_recover_prime_factors(n, e, d) if dmp1 is None: dmp1 = rsa_crt_dmp1(d, p) From d3a95bae9f8badb153dcd1f7d7e7052c6b88503a Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 14:26:53 -0700 Subject: [PATCH 15/29] Update test --- .vscode/cspell.json | 3 +-- sdk/keyvault/azure-keyvault-keys/assets.json | 2 +- sdk/keyvault/azure-keyvault-keys/tests/conftest.py | 10 +++++----- .../azure-keyvault-keys/tests/test_crypto_client.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 09e89d00293b..6970bee9583e 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -825,8 +825,7 @@ { "filename": "sdk/keyvault/**", "words": [ - "eddsa", - "asym" + "eddsa" ] }, { diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index ebe6a9f07daa..1e0516de62c6 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_95708a33a7" + "Tag": "python/keyvault/azure-keyvault-keys_d76555ec5f" } diff --git a/sdk/keyvault/azure-keyvault-keys/tests/conftest.py b/sdk/keyvault/azure-keyvault-keys/tests/conftest.py index da1dda822076..96181af7c1b1 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/conftest.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/conftest.py @@ -28,13 +28,13 @@ def pytest_configure(config): @pytest.fixture(scope="session", autouse=True) def add_sanitizers(test_proxy): - azure_keyvault_url = os.getenv("azure_keyvault_url", "https://vaultname.vault.azure.net") + azure_keyvault_url = os.getenv("AZURE_KEYVAULT_URL", "https://vaultname.vault.azure.net") azure_keyvault_url = azure_keyvault_url.rstrip("/") - keyvault_tenant_id = os.getenv("keyvault_tenant_id", "keyvault_tenant_id") - keyvault_subscription_id = os.getenv("keyvault_subscription_id", "keyvault_subscription_id") - azure_managedhsm_url = os.environ.get("azure_managedhsm_url","https://managedhsmvaultname.managedhsm.azure.net") + keyvault_tenant_id = os.getenv("KEYVAULT_TENANT_ID", "keyvault_tenant_id") + keyvault_subscription_id = os.getenv("KEYVAULT_SUBSCRIPTION_ID", "keyvault_subscription_id") + azure_managedhsm_url = os.environ.get("AZURE_MANAGEDHSM_URL","https://managedhsmvaultname.managedhsm.azure.net") azure_managedhsm_url = azure_managedhsm_url.rstrip("/") - azure_attestation_uri = os.environ.get("azure_keyvault_attestation_url","https://fakeattestation.azurewebsites.net") + azure_attestation_uri = os.environ.get("AZURE_KEYVAULT_ATTESTATION_URL","https://fakeattestation.azurewebsites.net") azure_attestation_uri = azure_attestation_uri.rstrip('/') add_general_regex_sanitizer(regex=azure_keyvault_url, value="https://vaultname.vault.azure.net") 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 5d370c9bf22d..a60cf7aa8695 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -207,7 +207,7 @@ def test_encrypt_and_decrypt_with_managed_key(self, key_client, **kwargs): assert result.key_id == imported_key.id # Create a KeyVaultRSAPrivateKey that can perform decryption with `cryptography`'s interface - managed_key = KeyVaultRSAPrivateKey(key_name=key_name, key_client=key_client) + managed_key = crypto_client.create_rsa_private_key() # We used RSA-OAEP to encrypt, so we use MGF1 and SHA1 as inputs to OAEP to match during decryption algorithm = SHA1() mgf = MGF1(algorithm) From ca5e6c57eaf7878a8fbfc71d6e54e52c5ebdc1bf Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 14:46:29 -0700 Subject: [PATCH 16/29] Add unimplemented signer method from mindep --- .../azure/keyvault/keys/crypto/_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index c84ac33a7a55..32df850c10dc 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -198,7 +198,11 @@ def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-r format: PrivateFormat, encryption_algorithm: KeySerializationEncryption, ) -> NoReturn: - """Returns the key serialized as bytes. Not implemented, as the private key is managed by Key Vault.""" + """Not implemented.""" + raise NotImplementedError() + + def signer(self, padding: AsymmetricPadding, algorithm: HashAlgorithm) -> NoReturn: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" raise NotImplementedError() From 6b6881d408754778a48419d6b3279dfdb6f4fc28 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 14:50:25 -0700 Subject: [PATCH 17/29] Pylint --- .../azure/keyvault/keys/crypto/_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index dc971e2d8cf5..6fa813b2d6a9 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py @@ -213,10 +213,13 @@ def _initialize(self, **kwargs) -> None: self._initialized = self._keys_get_forbidden @distributed_trace - def create_rsa_private_key(self) -> KeyVaultRSAPrivateKey: + 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: :class:`~azure.keyvault.keys.crypto.KeyVaultRSAPrivateKey` """ self._initialize() return KeyVaultRSAPrivateKey(client=self, key_material=cast(JsonWebKey, self._key)) From d8a526d9958c79e878e47f1eb14484b6d95ce9cd Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 14 Sep 2023 15:07:36 -0700 Subject: [PATCH 18/29] Add empty impl of RSAPublicKey --- .../azure/keyvault/keys/crypto/__init__.py | 2 + .../azure/keyvault/keys/crypto/_client.py | 14 ++- .../azure/keyvault/keys/crypto/_models.py | 88 +++++++++++++++++-- 3 files changed, 97 insertions(+), 7 deletions(-) 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 index 95629c2daa72..9e931898fc8e 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/__init__.py @@ -6,6 +6,7 @@ DecryptResult, EncryptResult, KeyVaultRSAPrivateKey, + KeyVaultRSAPublicKey, SignResult, WrapResult, VerifyResult, @@ -21,6 +22,7 @@ "EncryptionAlgorithm", "EncryptResult", "KeyVaultRSAPrivateKey", + "KeyVaultRSAPublicKey", "KeyWrapAlgorithm", "SignatureAlgorithm", "SignResult", 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 index 6fa813b2d6a9..efa51bd4f438 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_client.py @@ -10,7 +10,7 @@ from . import DecryptResult, EncryptionAlgorithm, EncryptResult, SignResult, VerifyResult, UnwrapResult, WrapResult from ._key_validity import raise_if_time_invalid -from ._models import KeyVaultRSAPrivateKey +from ._models import KeyVaultRSAPrivateKey, KeyVaultRSAPublicKey from ._providers import get_local_cryptography_provider, NoLocalCryptography from .. import KeyOperation from .._models import JsonWebKey, KeyVaultKey @@ -224,6 +224,18 @@ def create_rsa_private_key(self) -> KeyVaultRSAPrivateKey: # pylint:disable=cli 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: :class:`~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, **kwargs) -> EncryptResult: """Encrypt bytes using the client's key. 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 index 32df850c10dc..d07674aecb80 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -17,7 +17,12 @@ ) 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 +from cryptography.hazmat.primitives.serialization import ( + Encoding, + KeySerializationEncryption, + PrivateFormat, + PublicFormat, +) from ._enums import EncryptionAlgorithm, KeyWrapAlgorithm, SignatureAlgorithm from .._models import JsonWebKey @@ -32,10 +37,7 @@ SHA384: SignatureAlgorithm.rs384, SHA512: SignatureAlgorithm.rs512, } -OAEP_MAP = { - SHA1: EncryptionAlgorithm.rsa_oaep, - SHA256: EncryptionAlgorithm.rsa_oaep_256 -} +OAEP_MAP = {SHA1: EncryptionAlgorithm.rsa_oaep, SHA256: EncryptionAlgorithm.rsa_oaep_256} PSS_MAP = { SignatureAlgorithm.rs256: SignatureAlgorithm.ps256, SignatureAlgorithm.rs384: SignatureAlgorithm.ps384, @@ -201,7 +203,81 @@ def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-r """Not implemented.""" raise NotImplementedError() - def signer(self, padding: AsymmetricPadding, algorithm: HashAlgorithm) -> NoReturn: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + def signer( + self, padding: AsymmetricPadding, algorithm: HashAlgorithm + ) -> NoReturn: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" + raise NotImplementedError() + + +class KeyVaultRSAPublicKey(RSAPublicKey): + """An `RSAPublicKey` implementation based on a key managed by Key Vault.""" + + def __init__(self, client: "CryptographyClient", key_material: JsonWebKey) -> None: + """Creates a `KeyVaultRSAPublicKey` from a `CryptographyClient` and key. + + :param client: The client that will be used to communicate with Key Vault. + :type client: :class:`~azure.keyvault.keys.crypto.CryptographyClient` + :param key_material: They Key Vault key's material, as a `JsonWebKey`. + :type key_material: :class:`~azure.keyvault.keys.JsonWebKey` + """ + self._client: "CryptographyClient" = client + self._key: JsonWebKey = key_material + + def encrypt(self, plaintext: bytes, padding: AsymmetricPadding) -> bytes: + """ + Encrypts the given plaintext. + """ + + @property + def key_size(self) -> int: + """ + The bit length of the public modulus. + """ + + def public_numbers(self) -> RSAPublicNumbers: + """ + Returns an RSAPublicNumbers + """ + + def public_bytes( + self, + encoding: Encoding, + format: PublicFormat, + ) -> bytes: + """ + Returns the key serialized as bytes. + """ + + def verify( + self, + signature: bytes, + data: bytes, + padding: AsymmetricPadding, + algorithm: Union[Prehashed, HashAlgorithm], + ) -> None: + """ + Verifies the signature of the data. + """ + + def recover_data_from_signature( + self, + signature: bytes, + padding: AsymmetricPadding, + algorithm: Optional[HashAlgorithm], + ) -> bytes: + """ + Recovers the original data from the signature. + """ + + def __eq__(self, other: object) -> bool: + """ + Checks equality. + """ + + def verifier( + self, signature: bytes, padding: AsymmetricPadding, algorithm: HashAlgorithm + ) -> NoReturn: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" raise NotImplementedError() From b2053061cb6f2f9a7fa47aa5a20bab429545c3e7 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Fri, 15 Sep 2023 16:28:20 -0700 Subject: [PATCH 19/29] Initial impl of public key --- .../azure/keyvault/keys/crypto/_models.py | 325 +++++++++++------- 1 file changed, 198 insertions(+), 127 deletions(-) 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 index d07674aecb80..ea9f5f997523 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -4,6 +4,7 @@ # ------------------------------------ from typing import 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, @@ -45,6 +46,191 @@ } +def get_encryption_algorithm(padding: AsymmetricPadding) -> EncryptionAlgorithm: + """Maps an `AsymmetricPadding` to an encryption algorithm. + + :param padding: The padding to use. + :type 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: AsymmetricPadding + :param algorithm: The algorithm to use. + :type algorithm: 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.""" + + def __init__(self, client: "CryptographyClient", key_material: JsonWebKey) -> None: + """Creates a `KeyVaultRSAPublicKey` from a `CryptographyClient` and key. + + :param client: The client that will be used to communicate with Key Vault. + :type client: :class:`~azure.keyvault.keys.crypto.CryptographyClient` + :param key_material: They Key Vault key's material, as a `JsonWebKey`. + :type key_material: :class:`~azure.keyvault.keys.JsonWebKey` + """ + self._client: "CryptographyClient" = client + self._key: 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: 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 + """ + return len(self._key.n) * 8 # type: ignore[attr-defined] + + def public_numbers(self) -> RSAPublicNumbers: + """Returns an `RSAPublicNumbers`.""" + 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( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + self, + encoding: Encoding, + format: PublicFormat, + ) -> NoReturn: + """Not implemented.""" + raise NotImplementedError() + + 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: AsymmetricPadding + :param algorithm: The algorithm to sign with. Only `HashAlgorithm`s are supported -- specifically, `SHA256`, + `SHA384`, and `SHA512`. + :type algorithm: Prehashed or 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.decode()}' is invalid.") + + def recover_data_from_signature( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + self, + signature: bytes, + padding: AsymmetricPadding, + algorithm: Optional[HashAlgorithm], + ) -> NoReturn: + """Not implemented.""" + raise NotImplementedError() + + 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 otherwise. + :rtype: bool + """ + 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 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.""" @@ -71,28 +257,7 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: :returns: The decrypted plaintext, as bytes. :rtype: bytes """ - 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}") + mapped_algorithm = get_encryption_algorithm(padding) result = self._client.decrypt(mapped_algorithm, ciphertext) return result.plaintext @@ -105,16 +270,15 @@ def key_size(self) -> int: """ return len(self._key.n) * 8 # type: ignore[attr-defined] - def public_key(self) -> RSAPublicKey: - """The `RSAPublicKey` associated with this private key. + 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 `RSAPublicKey` associated with the key. - :rtype: RSAPublicKey + :returns: The `KeyVaultRSAPublicKey` associated with the key. + :rtype: :class:`~azure.keyvault.keys.crypto.KeyVaultRSAPublicKey` """ - 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) - return public_numbers.public_key() + return KeyVaultRSAPublicKey(self._client, self._key) def sign( self, @@ -138,31 +302,10 @@ def sign( """ if isinstance(algorithm, Prehashed): raise ValueError("`Prehashed` algorithms are unsupported. Please provide a `HashAlgorithm` instead.") - - 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}") - + mapped_algorithm = get_signature_algorithm(padding, algorithm) digest = Hash(algorithm) digest.update(data) - result = self._client.sign(cast(SignatureAlgorithm, mapped_algorithm), digest.finalize()) + result = self._client.sign(mapped_algorithm, digest.finalize()) return result.signature def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-missing-return,docstring-missing-rtype @@ -203,81 +346,9 @@ def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-r """Not implemented.""" raise NotImplementedError() - def signer( + def signer( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype self, padding: AsymmetricPadding, algorithm: HashAlgorithm - ) -> NoReturn: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" - raise NotImplementedError() - - -class KeyVaultRSAPublicKey(RSAPublicKey): - """An `RSAPublicKey` implementation based on a key managed by Key Vault.""" - - def __init__(self, client: "CryptographyClient", key_material: JsonWebKey) -> None: - """Creates a `KeyVaultRSAPublicKey` from a `CryptographyClient` and key. - - :param client: The client that will be used to communicate with Key Vault. - :type client: :class:`~azure.keyvault.keys.crypto.CryptographyClient` - :param key_material: They Key Vault key's material, as a `JsonWebKey`. - :type key_material: :class:`~azure.keyvault.keys.JsonWebKey` - """ - self._client: "CryptographyClient" = client - self._key: JsonWebKey = key_material - - def encrypt(self, plaintext: bytes, padding: AsymmetricPadding) -> bytes: - """ - Encrypts the given plaintext. - """ - - @property - def key_size(self) -> int: - """ - The bit length of the public modulus. - """ - - def public_numbers(self) -> RSAPublicNumbers: - """ - Returns an RSAPublicNumbers - """ - - def public_bytes( - self, - encoding: Encoding, - format: PublicFormat, - ) -> bytes: - """ - Returns the key serialized as bytes. - """ - - def verify( - self, - signature: bytes, - data: bytes, - padding: AsymmetricPadding, - algorithm: Union[Prehashed, HashAlgorithm], - ) -> None: - """ - Verifies the signature of the data. - """ - - def recover_data_from_signature( - self, - signature: bytes, - padding: AsymmetricPadding, - algorithm: Optional[HashAlgorithm], - ) -> bytes: - """ - Recovers the original data from the signature. - """ - - def __eq__(self, other: object) -> bool: - """ - Checks equality. - """ - - def verifier( - self, signature: bytes, padding: AsymmetricPadding, algorithm: HashAlgorithm - ) -> NoReturn: # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype + ) -> NoReturn: """Not implemented. This method was deprecated in `cryptography` 2.0 and removed in 37.0.0.""" raise NotImplementedError() From 131e1d27c11b90439c1861de5c066d4107e60347 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Fri, 15 Sep 2023 17:16:07 -0700 Subject: [PATCH 20/29] Add signing test --- sdk/keyvault/azure-keyvault-keys/assets.json | 2 +- .../azure/keyvault/keys/crypto/_models.py | 2 +- .../tests/test_crypto_client.py | 39 ++++++++++++++----- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index 1e0516de62c6..e1ebc1b59656 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_d76555ec5f" + "Tag": "python/keyvault/azure-keyvault-keys_59169b546d" } 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 index ea9f5f997523..1491c863feba 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -198,7 +198,7 @@ def verify( digest.update(data) result = self._client.verify(mapped_algorithm, digest.finalize(), signature) if not result.is_valid: - raise InvalidSignature(f"The provided signature '{signature.decode()}' is invalid.") + raise InvalidSignature(f"The provided signature '{signature!r}' is invalid.") def recover_data_from_signature( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype self, 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 a60cf7aa8695..f56589897330 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -11,8 +11,8 @@ from devtools_testutils import recorded_by_proxy, set_bodiless_matcher -from cryptography.hazmat.primitives.hashes import SHA1 -from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP +from cryptography.hazmat.primitives.hashes import SHA1, SHA256 +from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PSS import pytest from azure.core.exceptions import AzureError, HttpResponseError from azure.core.pipeline.policies import SansIOHTTPPolicy @@ -21,7 +21,6 @@ from azure.keyvault.keys.crypto import ( CryptographyClient, EncryptionAlgorithm, - KeyVaultRSAPrivateKey, KeyWrapAlgorithm, SignatureAlgorithm, ) @@ -203,16 +202,16 @@ def test_encrypt_and_decrypt_with_managed_key(self, key_client, **kwargs): imported_key = self._import_test_key(key_client, key_name) crypto_client = self.create_crypto_client(imported_key.id, api_version=key_client.api_version) - result = crypto_client.encrypt(EncryptionAlgorithm.rsa_oaep, self.plaintext) - assert result.key_id == imported_key.id - - # Create a KeyVaultRSAPrivateKey that can perform decryption with `cryptography`'s interface - managed_key = crypto_client.create_rsa_private_key() - # We used RSA-OAEP to encrypt, so we use MGF1 and SHA1 as inputs to OAEP to match during decryption + # Create a KeyVaultRSAPublicKey that can perform encryption with `cryptography`'s interface + public_key = crypto_client.create_rsa_public_key() algorithm = SHA1() mgf = MGF1(algorithm) padding = OAEP(mgf, algorithm, None) - plaintext = managed_key.decrypt(ciphertext=result.ciphertext, padding=padding) + ciphertext = public_key.encrypt(self.plaintext, padding) + + # Create a KeyVaultRSAPrivateKey that can perform decryption with `cryptography`'s interface + private_key = crypto_client.create_rsa_private_key() + plaintext = private_key.decrypt(ciphertext=ciphertext, padding=padding) assert self.plaintext == plaintext @pytest.mark.parametrize("api_version,is_hsm", no_get) @@ -236,6 +235,26 @@ def test_sign_and_verify(self, key_client, is_hsm, **kwargs): assert result.algorithm == SignatureAlgorithm.rs256 assert verified.is_valid + @pytest.mark.parametrize("api_version,is_hsm", only_vault_latest) + @KeysClientPreparer(permissions=NO_GET) + @recorded_by_proxy + def test_sign_and_verify_with_managed_key(self, key_client, is_hsm, **kwargs): + key_name = self.get_resource_name("keysign") + + imported_key = self._import_test_key(key_client, key_name, hardware_protected=is_hsm) + crypto_client = self.create_crypto_client(imported_key.id, api_version=key_client.api_version) + + # Create a KeyVaultRSAPrivateKey that can perform signing with `cryptography`'s interface + private_key = crypto_client.create_rsa_private_key() + algorithm = SHA256() + mgf = MGF1(algorithm) + padding = PSS(mgf, PSS.MAX_LENGTH) + signature = private_key.sign(self.plaintext, padding, algorithm) + + # Create a KeyVaultRSAPublicKey that can perform verifying with `cryptography`'s interface + public_key = crypto_client.create_rsa_public_key() + public_key.verify(signature, self.plaintext, padding, algorithm) + @pytest.mark.parametrize("api_version,is_hsm", no_get) @KeysClientPreparer(permissions=NO_GET) @recorded_by_proxy From 3761dbc7554c230941ea10c198d9e15edd8d70d2 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 25 Sep 2023 15:32:23 -0700 Subject: [PATCH 21/29] Docstrings; more tests --- .../azure/keyvault/keys/crypto/_models.py | 14 ++- .../tests/test_crypto_client.py | 101 +++++++++++++----- 2 files changed, 83 insertions(+), 32 deletions(-) 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 index 1491c863feba..94a302f248ef 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -157,7 +157,11 @@ def key_size(self) -> int: return len(self._key.n) * 8 # type: ignore[attr-defined] def public_numbers(self) -> RSAPublicNumbers: - """Returns an `RSAPublicNumbers`.""" + """Returns an `RSAPublicNumbers` representing the key's public numbers. + + :returns: The public numbers of the key. + :rtype: RSAPublicNumbers + """ 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) @@ -308,8 +312,12 @@ def sign( result = self._client.sign(mapped_algorithm, digest.finalize()) return result.signature - def private_numbers(self) -> RSAPrivateNumbers: # pylint:disable=docstring-missing-return,docstring-missing-rtype - """Returns an `RSAPrivateNumbers`.""" + def private_numbers(self) -> RSAPrivateNumbers: + """Returns an `RSAPrivateNumbers` representing the key's private numbers. + + :returns: The private numbers of the key. + :rtype: RSAPrivateNumbers + """ # 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] 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 f56589897330..b980c5a8b395 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -13,6 +13,7 @@ from cryptography.hazmat.primitives.hashes import SHA1, SHA256 from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PSS +from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp import pytest from azure.core.exceptions import AzureError, HttpResponseError from azure.core.pipeline.policies import SansIOHTTPPolicy @@ -43,6 +44,32 @@ no_get = get_decorator(permissions=NO_GET) +def _to_bytes(hex): + if len(hex) % 2: + hex = f"0{hex}" + return codecs.decode(hex, "hex_codec") + + +# RSA key with private components so that the JWK could theoretically be used for private operations +TEST_JWK = { + "kty":"RSA", + "key_ops":["decrypt", "verify", "unwrapKey"], + "n":_to_bytes( + "00a0914d00234ac683b21b4c15d5bed887bdc959c2e57af54ae734e8f00720d775d275e455207e3784ceeb60a50a4655dd72a7a94d271e8ee8f7959a669ca6e775bf0e23badae991b4529d978528b4bd90521d32dd2656796ba82b6bbfc7668c8f5eeb5053747fd199319d29a8440d08f4412d527ff9311eda71825920b47b1c46b11ab3e91d7316407e89c7f340f7b85a34042ce51743b27d4718403d34c7b438af6181be05e4d11eb985d38253d7fe9bf53fc2f1b002d22d2d793fa79a504b6ab42d0492804d7071d727a06cf3a8893aa542b1503f832b296371b6707d4dc6e372f8fe67d8ded1c908fde45ce03bc086a71487fa75e43aa0e0679aa0d20efe35" + ), + "e":_to_bytes("10001"), + "p":_to_bytes( + "00d1deac8d68ddd2c1fd52d5999655b2cf1565260de5269e43fd2a85f39280e1708ffff0682166cb6106ee5ea5e9ffd9f98d0becc9ff2cda2febc97259215ad84b9051e563e14a051dce438bc6541a24ac4f014cf9732d36ebfc1e61a00d82cbe412090f7793cfbd4b7605be133dfc3991f7e1bed5786f337de5036fc1e2df4cf3" + ), + "q":_to_bytes( + "00c3dc66b641a9b73cd833bc439cd34fc6574465ab5b7e8a92d32595a224d56d911e74624225b48c15a670282a51c40d1dad4bc2e9a3c8dab0c76f10052dfb053bc6ed42c65288a8e8bace7a8881184323f94d7db17ea6dfba651218f931a93b8f738f3d8fd3f6ba218d35b96861a0f584b0ab88ddcf446b9815f4d287d83a3237" + ), + "d":_to_bytes( + "627c7d24668148fe2252c7fa649ea8a5a9ed44d75c766cda42b29b660e99404f0e862d4561a6c95af6a83d213e0a2244b03cd28576473215073785fb067f015da19084ade9f475e08b040a9a2c7ba00253bb8125508c9df140b75161d266be347a5e0f6900fe1d8bbf78ccc25eeb37e0c9d188d6e1fc15169ba4fe12276193d77790d2326928bd60d0d01d6ead8d6ac4861abadceec95358fd6689c50a1671a4a936d2376440a41445501da4e74bfb98f823bd19c45b94eb01d98fc0d2f284507f018ebd929b8180dbe6381fdd434bffb7800aaabdd973d55f9eaf9bb88a6ea7b28c2a80231e72de1ad244826d665582c2362761019de2e9f10cb8bcc2625649" + ) +} + + class TestCryptoClient(KeyVaultTestCase, KeysTestCase): plaintext = b"5063e6aaa845f150200547944fd199679c98ed6f99da0a0b2dafeaf1f4684496fd532c1c229968cb9dee44957fcef7ccef59ceda0b362e56bcd78fd3faee5781c623c0bb22b35beabde0664fd30e0e824aba3dd1b0afffc4a3d955ede20cf6a854d52cfd" iv = codecs.decode("89b8adbfb07345e3598932a09c517441", "hex_codec") @@ -90,11 +117,6 @@ def _validate_ec_key_bundle(self, key_curve, key_attributes, vault, key_name, kt assert key_attributes.properties.created_on and key_attributes.properties.updated_on,"Missing required date attributes." def _import_test_key(self, client, name, hardware_protected=False): - def _to_bytes(hex): - if len(hex) % 2: - hex = f"0{hex}" - return codecs.decode(hex, "hex_codec") - key = JsonWebKey( kty="RSA-HSM" if hardware_protected else "RSA", key_ops=["encrypt", "decrypt", "sign", "verify", "wrapKey", "unwrapKey"], @@ -795,30 +817,8 @@ def test_local_only_mode_no_service_calls(): def test_local_only_mode_raise(): """A local-only CryptographyClient should raise an exception if an operation can't be performed locally""" - def _to_bytes(hex): - if len(hex) % 2: - hex = f"0{hex}" - return codecs.decode(hex, "hex_codec") - - # Create an RSA key with private components so that the JWK could theoretically be used for private operations - jwk = { - "kty":"RSA", - "key_ops":["decrypt", "verify", "unwrapKey"], - "n":_to_bytes( - "00a0914d00234ac683b21b4c15d5bed887bdc959c2e57af54ae734e8f00720d775d275e455207e3784ceeb60a50a4655dd72a7a94d271e8ee8f7959a669ca6e775bf0e23badae991b4529d978528b4bd90521d32dd2656796ba82b6bbfc7668c8f5eeb5053747fd199319d29a8440d08f4412d527ff9311eda71825920b47b1c46b11ab3e91d7316407e89c7f340f7b85a34042ce51743b27d4718403d34c7b438af6181be05e4d11eb985d38253d7fe9bf53fc2f1b002d22d2d793fa79a504b6ab42d0492804d7071d727a06cf3a8893aa542b1503f832b296371b6707d4dc6e372f8fe67d8ded1c908fde45ce03bc086a71487fa75e43aa0e0679aa0d20efe35" - ), - "e":_to_bytes("10001"), - "p":_to_bytes( - "00d1deac8d68ddd2c1fd52d5999655b2cf1565260de5269e43fd2a85f39280e1708ffff0682166cb6106ee5ea5e9ffd9f98d0becc9ff2cda2febc97259215ad84b9051e563e14a051dce438bc6541a24ac4f014cf9732d36ebfc1e61a00d82cbe412090f7793cfbd4b7605be133dfc3991f7e1bed5786f337de5036fc1e2df4cf3" - ), - "q":_to_bytes( - "00c3dc66b641a9b73cd833bc439cd34fc6574465ab5b7e8a92d32595a224d56d911e74624225b48c15a670282a51c40d1dad4bc2e9a3c8dab0c76f10052dfb053bc6ed42c65288a8e8bace7a8881184323f94d7db17ea6dfba651218f931a93b8f738f3d8fd3f6ba218d35b96861a0f584b0ab88ddcf446b9815f4d287d83a3237" - ), - "d":_to_bytes( - "627c7d24668148fe2252c7fa649ea8a5a9ed44d75c766cda42b29b660e99404f0e862d4561a6c95af6a83d213e0a2244b03cd28576473215073785fb067f015da19084ade9f475e08b040a9a2c7ba00253bb8125508c9df140b75161d266be347a5e0f6900fe1d8bbf78ccc25eeb37e0c9d188d6e1fc15169ba4fe12276193d77790d2326928bd60d0d01d6ead8d6ac4861abadceec95358fd6689c50a1671a4a936d2376440a41445501da4e74bfb98f823bd19c45b94eb01d98fc0d2f284507f018ebd929b8180dbe6381fdd434bffb7800aaabdd973d55f9eaf9bb88a6ea7b28c2a80231e72de1ad244826d665582c2362761019de2e9f10cb8bcc2625649" - ) - } - client = CryptographyClient.from_jwk(jwk=jwk) + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) # Algorithm not supported locally with pytest.raises(NotImplementedError) as ex: @@ -977,6 +977,49 @@ def test_decrypt_argument_validation(): assert "iv" in str(ex.value) and "required" in str(ex.value) +def test_rsa_public_key_public_numbers(): + """Verify behavior of KeyVaultRSAPublicKey.public_numbers""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + public_key = client.create_rsa_public_key() + public_numbers = public_key.public_numbers() + assert public_numbers.e == int.from_bytes(TEST_JWK["e"], "big") + assert public_numbers.n == int.from_bytes(TEST_JWK["n"], "big") + + +def test_rsa_public_key_equals(): + """Verify behavior of KeyVaultRSAPublicKey.__eq__ against a JWK and KeyVaultRSAPublicKey instance""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + public_key = client.create_rsa_public_key() + assert public_key == JsonWebKey(**TEST_JWK) + key_dupe = client.create_rsa_public_key() + assert public_key == key_dupe + + +def test_rsa_private_key_public_key(): + """Verify behavior of KeyVaultRSAPrivateKey.public_key against a JWK and KeyVaultRSAPublicKey instance""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + public_key = client.create_rsa_public_key() + private_key = client.create_rsa_private_key() + assert private_key.public_key() == public_key + + +def test_rsa_private_key_private_numbers(): + """Verify behavior of KeyVaultRSAPrivateKey.private_numbers""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + private_key = client.create_rsa_private_key() + private_numbers = private_key.private_numbers() + assert private_numbers.d == int.from_bytes(TEST_JWK["d"], "big") + assert private_numbers.p == int.from_bytes(TEST_JWK["p"], "big") + assert private_numbers.q == int.from_bytes(TEST_JWK["q"], "big") + assert private_numbers.dmp1 == rsa_crt_dmp1(private_numbers.d, private_numbers.p) + assert private_numbers.dmq1 == rsa_crt_dmq1(private_numbers.d, private_numbers.q) + assert private_numbers.iqmp == rsa_crt_iqmp(private_numbers.p, private_numbers.q) + + def test_retain_url_port(): """Regression test for https://github.com/Azure/azure-sdk-for-python/issues/24446""" From 3b50b96918832dc2cf2ee02276bd3b6c5987c954 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 25 Sep 2023 19:47:55 -0700 Subject: [PATCH 22/29] Update changelog --- sdk/keyvault/azure-keyvault-keys/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md index bfb53c243214..e1ffe06afb16 100644 --- a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md @@ -3,6 +3,10 @@ ## 4.9.0b2 (Unreleased) ### Features Added +- The `cryptography` library's `RSAPrivateKey` and `RSAPublicKey` interfaces are now implemented by + `KeyVaultRSAPrivateKey` and `KeyVaultRSAPublicKey` classes that use keys managed by Key Vault +- `CryptographyClient` has `create_rsa_private_key` and `create_rsa_public_key` methods that return a + `KeyVaultRSAPrivateKey` and `KeyVaultRSAPublicKey`, respectively ### Breaking Changes From 98fa2f4dab3c78923c1dd9f81f5bc53f7b31ab89 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Mon, 2 Oct 2023 23:06:13 -0700 Subject: [PATCH 23/29] Test impls against cryptography's --- sdk/keyvault/azure-keyvault-keys/assets.json | 2 +- .../tests/test_crypto_client.py | 30 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index e1ebc1b59656..18d80a920220 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_59169b546d" + "Tag": "python/keyvault/azure-keyvault-keys_02344e7a47" } 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 b980c5a8b395..31fa6724107d 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -12,7 +12,7 @@ from devtools_testutils import recorded_by_proxy, set_bodiless_matcher from cryptography.hazmat.primitives.hashes import SHA1, SHA256 -from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PSS +from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15 from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp import pytest from azure.core.exceptions import AzureError, HttpResponseError @@ -50,7 +50,7 @@ def _to_bytes(hex): return codecs.decode(hex, "hex_codec") -# RSA key with private components so that the JWK could theoretically be used for private operations +# RSA key with private components so that the JWK can be used for private operations TEST_JWK = { "kty":"RSA", "key_ops":["decrypt", "verify", "unwrapKey"], @@ -236,6 +236,15 @@ def test_encrypt_and_decrypt_with_managed_key(self, key_client, **kwargs): plaintext = private_key.decrypt(ciphertext=ciphertext, padding=padding) assert self.plaintext == plaintext + # Use cryptography library's own implementation to validate ours (as well as our public/private numbers) + crypto_public_key = public_key.public_numbers().public_key() + crypto_ciphertext = crypto_public_key.encrypt(self.plaintext, padding) + # Create a crypto client from private JWK since we can't get the private components from an imported key + crypto_client = CryptographyClient.from_jwk(jwk=TEST_JWK) + crypto_private_key = crypto_client.create_rsa_private_key().private_numbers().private_key() + crypto_plaintext = crypto_private_key.decrypt(ciphertext=crypto_ciphertext, padding=padding) + assert crypto_plaintext == plaintext + @pytest.mark.parametrize("api_version,is_hsm", no_get) @KeysClientPreparer(permissions=NO_GET) @recorded_by_proxy @@ -269,14 +278,27 @@ def test_sign_and_verify_with_managed_key(self, key_client, is_hsm, **kwargs): # Create a KeyVaultRSAPrivateKey that can perform signing with `cryptography`'s interface private_key = crypto_client.create_rsa_private_key() algorithm = SHA256() - mgf = MGF1(algorithm) - padding = PSS(mgf, PSS.MAX_LENGTH) + padding = PKCS1v15() signature = private_key.sign(self.plaintext, padding, algorithm) # Create a KeyVaultRSAPublicKey that can perform verifying with `cryptography`'s interface public_key = crypto_client.create_rsa_public_key() public_key.verify(signature, self.plaintext, padding, algorithm) + # Use cryptography library's own implementation to validate ours (as well as our public/private numbers) + # Create a crypto client from private JWK since we can't get the private components from an imported key + crypto_client = CryptographyClient.from_jwk(jwk=TEST_JWK) + crypto_private_key = crypto_client.create_rsa_private_key().private_numbers().private_key() + crypto_signature = crypto_private_key.sign(self.plaintext, padding, algorithm) + + # PKCS#1 signing produces deterministic signatures, so we can compare the two signatures we generated + # PSS padding is nondeterministic, by comparison + assert signature == crypto_signature + + crypto_public_key = public_key.public_numbers().public_key() + crypto_public_key.verify(crypto_signature, self.plaintext, padding, algorithm) + crypto_public_key.verify(signature, self.plaintext, padding, algorithm) + @pytest.mark.parametrize("api_version,is_hsm", no_get) @KeysClientPreparer(permissions=NO_GET) @recorded_by_proxy From 74aaed1dc80295de9601c94d3a95d81c09163298 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Wed, 4 Oct 2023 17:08:01 -0700 Subject: [PATCH 24/29] Use crypto impls for non-KV operations --- .../azure/keyvault/keys/crypto/_models.py | 133 +++++++++++++----- .../tests/test_crypto_client.py | 50 ++++++- 2 files changed, 145 insertions(+), 38 deletions(-) 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 index 94a302f248ef..8dd63fc4fe36 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -50,7 +50,7 @@ def get_encryption_algorithm(padding: AsymmetricPadding) -> EncryptionAlgorithm: """Maps an `AsymmetricPadding` to an encryption algorithm. :param padding: The padding to use. - :type padding: AsymmetricPadding + :type padding: :class:`~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding` :returns: The corresponding Key Vault encryption algorithm. :rtype: EncryptionAlgorithm @@ -86,9 +86,9 @@ def get_signature_algorithm(padding: AsymmetricPadding, algorithm: HashAlgorithm """Maps an `AsymmetricPadding` and `HashAlgorithm` to a signature algorithm. :param padding: The padding to use. - :type padding: AsymmetricPadding + :type padding: :class:`~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding` :param algorithm: The algorithm to use. - :type algorithm: HashAlgorithm + :type algorithm: :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` :returns: The corresponding Key Vault signature algorithm. :rtype: SignatureAlgorithm @@ -138,7 +138,7 @@ def encrypt(self, plaintext: bytes, padding: AsymmetricPadding) -> bytes: :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: AsymmetricPadding + :type padding: :class:`~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding` :returns: The encrypted ciphertext, as bytes. :rtype: bytes @@ -154,7 +154,8 @@ def key_size(self) -> int: :returns: The key's size. :rtype: int """ - return len(self._key.n) * 8 # type: ignore[attr-defined] + 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. @@ -166,13 +167,23 @@ def public_numbers(self) -> RSAPublicNumbers: n = int.from_bytes(self._key.n, "big") # type: ignore[attr-defined] return RSAPublicNumbers(e, n) - def public_bytes( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - self, - encoding: Encoding, - format: PublicFormat, - ) -> NoReturn: - """Not implemented.""" - raise NotImplementedError() + 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: :class:`~cryptography.hazmat.primitives.serialization.Encoding` + :param format: A value from the `PublicFormat` enum. + :type format: :class:`~cryptography.hazmat.primitives.serialization.PublicFormat` + + :returns: The serialized key. + :rtype: bytes + """ + public_key = self.public_numbers().public_key() + return public_key.public_bytes(encoding=encoding, format=format) def verify( self, @@ -188,10 +199,11 @@ def verify( :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: AsymmetricPadding + :type padding: :class:`~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: Prehashed or HashAlgorithm + :type algorithm: :class:`~cryptography.hazmat.primitives.asymmetric.utils.Prehashed` or + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` :raises InvalidSignature: If the signature does not validate. """ @@ -204,14 +216,50 @@ def verify( if not result.is_valid: raise InvalidSignature(f"The provided signature '{signature!r}' is invalid.") - def recover_data_from_signature( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - self, - signature: bytes, - padding: AsymmetricPadding, - algorithm: Optional[HashAlgorithm], - ) -> NoReturn: - """Not implemented.""" - raise NotImplementedError() + def recover_data_from_signature( + self, signature: bytes, padding: AsymmetricPadding, algorithm: Optional[HashAlgorithm] + ) -> bytes: + """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: :class:`~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: :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` + + :returns: The signed data. + :rtype: bytes + :raises: + NotImplementedError if the local version of `cryptography` doesn't support this method. + :class:`~cryptography.exceptions.InvalidSignature` if the signature is invalid. + :class:`~cryptography.exceptions.UnsupportedAlgorithm` if the signature data recovery is not supported with + the provided `padding` type. + """ + public_key = self.public_numbers().public_key() + try: + return public_key.recover_data_from_signature(signature=signature, padding=padding, algorithm=algorithm) + except AttributeError: + raise NotImplementedError( + "This method is only available on `cryptography`>=3.3. Update your package version to use this method." + ) def __eq__(self, other: object) -> bool: """Checks equality. @@ -256,7 +304,7 @@ def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes: :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: AsymmetricPadding + :type padding: :class:`~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding` :returns: The decrypted plaintext, as bytes. :rtype: bytes @@ -272,7 +320,8 @@ def key_size(self) -> int: :returns: The key's size. :rtype: int """ - return len(self._key.n) * 8 # type: ignore[attr-defined] + private_key = self.private_numbers().private_key() + return private_key.key_size def public_key(self) -> KeyVaultRSAPublicKey: """The `RSAPublicKey` associated with this private key, as a `KeyVaultRSAPublicKey`. @@ -296,10 +345,11 @@ def sign( :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: AsymmetricPadding + :type padding: :class:`~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: Prehashed or HashAlgorithm + :type algorithm: :class:`~cryptography.hazmat.primitives.asymmetric.utils.Prehashed` or + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` :returns: The signature, as bytes. :rtype: bytes @@ -316,7 +366,7 @@ def private_numbers(self) -> RSAPrivateNumbers: """Returns an `RSAPrivateNumbers` representing the key's private numbers. :returns: The private numbers of the key. - :rtype: RSAPrivateNumbers + :rtype: :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateNumbers` """ # Fetch public numbers from JWK e = int.from_bytes(self._key.e, "big") # type: ignore[attr-defined] @@ -345,14 +395,27 @@ def private_numbers(self) -> RSAPrivateNumbers: return RSAPrivateNumbers(p, q, d, dmp1, dmq1, iqmp, public_numbers) - def private_bytes( # pylint:disable=docstring-missing-param,docstring-missing-return,docstring-missing-rtype - self, - encoding: Encoding, - format: PrivateFormat, - encryption_algorithm: KeySerializationEncryption, - ) -> NoReturn: - """Not implemented.""" - raise NotImplementedError() + 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: :class:`~cryptography.hazmat.primitives.serialization.Encoding` + :param format: A value from the `PrivateFormat` enum. + :type format: :class:`~cryptography.hazmat.primitives.serialization.PrivateFormat` + :param encryption_algorithm: An instance of an object conforming to the `KeySerializationEncryption` interface. + :type encryption_algorithm: :class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption` + + :returns: The serialized key. + :rtype: bytes + """ + private_key = self.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 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 31fa6724107d..26996b46dfc9 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -13,7 +13,14 @@ from cryptography.hazmat.primitives.hashes import SHA1, SHA256 from cryptography.hazmat.primitives.asymmetric.padding import MGF1, OAEP, PKCS1v15 -from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp +from cryptography.hazmat.primitives.asymmetric.rsa import ( + rsa_crt_dmp1, + rsa_crt_dmq1, + rsa_crt_iqmp, + RSAPrivateNumbers, + RSAPublicNumbers +) +from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat import pytest from azure.core.exceptions import AzureError, HttpResponseError from azure.core.pipeline.policies import SansIOHTTPPolicy @@ -288,14 +295,15 @@ def test_sign_and_verify_with_managed_key(self, key_client, is_hsm, **kwargs): # Use cryptography library's own implementation to validate ours (as well as our public/private numbers) # Create a crypto client from private JWK since we can't get the private components from an imported key crypto_client = CryptographyClient.from_jwk(jwk=TEST_JWK) - crypto_private_key = crypto_client.create_rsa_private_key().private_numbers().private_key() + private_numbers = crypto_client.create_rsa_private_key().private_numbers() + crypto_private_key = private_numbers.private_key() crypto_signature = crypto_private_key.sign(self.plaintext, padding, algorithm) # PKCS#1 signing produces deterministic signatures, so we can compare the two signatures we generated # PSS padding is nondeterministic, by comparison assert signature == crypto_signature - crypto_public_key = public_key.public_numbers().public_key() + crypto_public_key = private_numbers.public_numbers.public_key() crypto_public_key.verify(crypto_signature, self.plaintext, padding, algorithm) crypto_public_key.verify(signature, self.plaintext, padding, algorithm) @@ -1019,6 +1027,19 @@ def test_rsa_public_key_equals(): assert public_key == key_dupe +def test_rsa_public_key_public_bytes(): + """Verify behavior of KeyVaultRSAPublicKey.public_bytes""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + public_key = client.create_rsa_public_key() + public_bytes = public_key.public_bytes(Encoding.PEM, PublicFormat.PKCS1) + + public_numbers = public_key.public_numbers() + crypto_public_numbers = RSAPublicNumbers(e=public_numbers.e, n=public_numbers.n) + crypto_public_bytes = crypto_public_numbers.public_key().public_bytes(Encoding.PEM, PublicFormat.PKCS1) + assert public_bytes == crypto_public_bytes + + def test_rsa_private_key_public_key(): """Verify behavior of KeyVaultRSAPrivateKey.public_key against a JWK and KeyVaultRSAPublicKey instance""" @@ -1042,6 +1063,29 @@ def test_rsa_private_key_private_numbers(): assert private_numbers.iqmp == rsa_crt_iqmp(private_numbers.p, private_numbers.q) +def test_rsa_private_key_private_bytes(): + """Verify behavior of KeyVaultRSAPrivateKey.private_bytes""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + private_key = client.create_rsa_private_key() + private_bytes = private_key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()) + + private_numbers = private_key.private_numbers() + crypto_private_numbers = RSAPrivateNumbers( + p=private_numbers.p, + q=private_numbers.q, + d=private_numbers.d, + dmp1=private_numbers.dmp1, + dmq1=private_numbers.dmq1, + iqmp=private_numbers.iqmp, + public_numbers=private_numbers.public_numbers, + ) + crypto_private_bytes = crypto_private_numbers.private_key().private_bytes( + Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption() + ) + assert private_bytes == crypto_private_bytes + + def test_retain_url_port(): """Regression test for https://github.com/Azure/azure-sdk-for-python/issues/24446""" From a008d79f3750102970e2774d188567274bbb81f0 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Wed, 4 Oct 2023 17:10:29 -0700 Subject: [PATCH 25/29] Add synchronous limitation disclaimer --- .../azure/keyvault/keys/crypto/_models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 index 8dd63fc4fe36..cff3bd0b8d30 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -118,7 +118,10 @@ def get_signature_algorithm(padding: AsymmetricPadding, algorithm: HashAlgorithm class KeyVaultRSAPublicKey(RSAPublicKey): - """An `RSAPublicKey` implementation based on a key managed by Key Vault.""" + """An `RSAPublicKey` implementation based on a key managed by Key Vault. + + Only synchronous clients and operations are supported at this time. + """ def __init__(self, client: "CryptographyClient", key_material: JsonWebKey) -> None: """Creates a `KeyVaultRSAPublicKey` from a `CryptographyClient` and key. @@ -284,7 +287,10 @@ def verifier( # pylint:disable=docstring-missing-param,docstring-missing-return class KeyVaultRSAPrivateKey(RSAPrivateKey): - """An `RSAPrivateKey` implementation based on a key managed by Key Vault.""" + """An `RSAPrivateKey` implementation based on a key managed by Key Vault. + + Only synchronous clients and operations are supported at this time. + """ def __init__(self, client: "CryptographyClient", key_material: JsonWebKey) -> None: """Creates a `KeyVaultRSAPrivateKey` from a `CryptographyClient` and key. From 841ba4365dbcc75dfb405d622bc511ad1c6594fc Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 5 Oct 2023 14:59:02 -0700 Subject: [PATCH 26/29] cspell; pylint; synchronize recordings --- .vscode/cspell.json | 3 ++- sdk/keyvault/azure-keyvault-keys/assets.json | 2 +- .../azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 3a8f31c9c874..8b762e5c1b21 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -831,7 +831,8 @@ { "filename": "sdk/keyvault/**", "words": [ - "eddsa" + "eddsa", + "Thawte" ] }, { diff --git a/sdk/keyvault/azure-keyvault-keys/assets.json b/sdk/keyvault/azure-keyvault-keys/assets.json index 18d80a920220..ba822569c33e 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_02344e7a47" + "Tag": "python/keyvault/azure-keyvault-keys_8ef3422a55" } 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 index cff3bd0b8d30..085651369580 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -259,10 +259,10 @@ def recover_data_from_signature( public_key = self.public_numbers().public_key() try: return public_key.recover_data_from_signature(signature=signature, padding=padding, algorithm=algorithm) - except AttributeError: + 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. From 7179bfb971b76167fa963ddbb53fdb3a1ea58cab Mon Sep 17 00:00:00 2001 From: mccoyp Date: Thu, 5 Oct 2023 15:18:31 -0700 Subject: [PATCH 27/29] Raise 'private_bytes'-specific error --- .../azure/keyvault/keys/crypto/_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index 085651369580..059e0c29052e 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -420,7 +420,11 @@ def private_bytes( :returns: The serialized key. :rtype: bytes """ - private_key = self.private_numbers().private_key() + 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 From 7cac60106edf444d431d1c52d86c38ff06f194a4 Mon Sep 17 00:00:00 2001 From: mccoyp Date: Tue, 10 Oct 2023 14:30:17 -0700 Subject: [PATCH 28/29] Reliably get private key size --- .../azure/keyvault/keys/crypto/_models.py | 5 +++-- .../azure-keyvault-keys/tests/test_crypto_client.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) 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 index 059e0c29052e..021b01c139bc 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/crypto/_models.py @@ -326,8 +326,9 @@ def key_size(self) -> int: :returns: The key's size. :rtype: int """ - private_key = self.private_numbers().private_key() - return private_key.key_size + # 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`. 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 26996b46dfc9..e14d98cdb3b2 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_crypto_client.py @@ -1040,6 +1040,15 @@ def test_rsa_public_key_public_bytes(): assert public_bytes == crypto_public_bytes +def test_rsa_public_key_private_key_size(): + """Verify that KeyVaultRSAPublicKey.key_size and KeyVaultRSAPrivateKey.key_size are equal for the same key""" + + client = CryptographyClient.from_jwk(jwk=TEST_JWK) + public_key = client.create_rsa_public_key() + private_key = client.create_rsa_private_key() + assert public_key.key_size == private_key.key_size == 2048 + + def test_rsa_private_key_public_key(): """Verify behavior of KeyVaultRSAPrivateKey.public_key against a JWK and KeyVaultRSAPublicKey instance""" From dc813071985da6083f5d108f87ec5a6901d0586c Mon Sep 17 00:00:00 2001 From: mccoyp Date: Wed, 11 Oct 2023 10:57:13 -0700 Subject: [PATCH 29/29] Update changelog for release --- sdk/keyvault/azure-keyvault-keys/CHANGELOG.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md index e1ffe06afb16..964a40b8dae9 100644 --- a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md @@ -1,19 +1,13 @@ # Release History -## 4.9.0b2 (Unreleased) +## 4.9.0b2 (2023-10-12) ### Features Added - The `cryptography` library's `RSAPrivateKey` and `RSAPublicKey` interfaces are now implemented by - `KeyVaultRSAPrivateKey` and `KeyVaultRSAPublicKey` classes that use keys managed by Key Vault + `KeyVaultRSAPrivateKey` and `KeyVaultRSAPublicKey` classes that can use keys managed by Key Vault - `CryptographyClient` has `create_rsa_private_key` and `create_rsa_public_key` methods that return a `KeyVaultRSAPrivateKey` and `KeyVaultRSAPublicKey`, respectively -### Breaking Changes - -### Bugs Fixed - -### Other Changes - ## 4.9.0b1 (2023-05-16) ### Bugs Fixed