From 3247ed77f1a02d4fe91810a24632ed4dea1bfdcf Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 23 Mar 2023 11:12:35 +0100 Subject: [PATCH 1/3] key: refactor SSlibKey.verify_signature Signature verification for "securesystemslib keys" was previously implemented in 'rsa_keys', 'ecdsa_keys' and 'ed25519_keys' modules, which were called from `SSlibKey.verify_signature` via the legacy interface function `keys.verify_signature()`. This commit moves the entire implementation to SSlibKey, which will allow us (in a subsequent commit) to drastically decrease LOC count and drop 'nacl' optional dependency for ed25519 keys, in favour of 'pyca/cryptography', which we already use for all other sslib keys. This works as is with current version of python-tuf! An alternative design for this refactor used separate RSAKey, ECDSAKey and ED25510Key classes to replace SSlibKey, but that mostly added redundant boilerplate code. To the user it shouldn't matter, so let's do what makes sense from maintainer perspective. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 138 +++++++++++++++++++++++++++++--- 1 file changed, 125 insertions(+), 13 deletions(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index cc4a26c8..3dbf2260 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -1,12 +1,47 @@ """Key interface and the default implementations""" import logging from abc import ABCMeta, abstractmethod -from typing import Any, Dict, Optional, Tuple, Type +from typing import Any, Dict, Optional, Tuple, Type, cast -import securesystemslib.keys as sslib_keys from securesystemslib import exceptions +from securesystemslib._vendor.ed25519.ed25519 import ( + SignatureMismatch, + checkvalid, +) from securesystemslib.signer._signature import Signature +CRYPTO_IMPORT_ERROR = None +try: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric.ec import ( + ECDSA, + EllipticCurvePublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.padding import ( + MGF1, + PSS, + PKCS1v15, + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + AsymmetricPadding, + RSAPublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes + from cryptography.hazmat.primitives.hashes import ( + SHA224, + SHA256, + SHA384, + SHA512, + HashAlgorithm, + ) + from cryptography.hazmat.primitives.serialization import load_pem_public_key +except ImportError: + CRYPTO_IMPORT_ERROR = "'pyca/cryptography' library required" + + logger = logging.getLogger(__name__) # NOTE Key dispatch table is defined here so it's usable by Key, @@ -180,22 +215,99 @@ def from_dict(cls, keyid: str, key_dict: Dict[str, Any]) -> "SSlibKey": def to_dict(self) -> Dict[str, Any]: return self._to_dict() + def _from_pem(self) -> "PublicKeyTypes": + """Helper to load public key instance from PEM-formatted keyval.""" + public_bytes = self.keyval["public"].encode("utf-8") + return load_pem_public_key(public_bytes) + + @staticmethod + def _get_hash_algorithm(name: str) -> "HashAlgorithm": + """Helper to return hash algorithm for name.""" + algorithm: HashAlgorithm + if name == "sha224": + algorithm = SHA224() + if name == "sha256": + algorithm = SHA256() + if name == "sha384": + algorithm = SHA384() + if name == "sha512": + algorithm = SHA512() + + return algorithm + + @staticmethod + def _get_rsa_padding( + name: str, hash_algorithm: "HashAlgorithm" + ) -> "AsymmetricPadding": + """Helper to return rsa signature padding for name.""" + padding: AsymmetricPadding + if name == "pss": + padding = PSS(mgf=MGF1(hash_algorithm), salt_length=PSS.AUTO) + + if name == "pkcs1v15": + padding = PKCS1v15() + + return padding + def verify_signature(self, signature: Signature, data: bytes) -> None: try: - if not sslib_keys.verify_signature( - self.to_securesystemslib_key(), - signature.to_dict(), - data, + if signature.keyid != self.keyid: + raise ValueError( + f"keyid mismatch: 'key id: {self.keyid}" + f" != signature keyid: {signature.keyid}'" + ) + sig = bytes.fromhex(signature.signature) + + if CRYPTO_IMPORT_ERROR: + if self.scheme == "ed25519": + public_bytes = bytes.fromhex(self.keyval["public"]) + checkvalid(sig, data, public_bytes) + return + + raise exceptions.UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + key: PublicKeyTypes + if self.scheme in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + key = cast(RSAPublicKey, self._from_pem()) + padding_name, hash_name = self.scheme.split("-")[1:] + hash_algorithm = self._get_hash_algorithm(hash_name) + padding = self._get_rsa_padding(padding_name, hash_algorithm) + key.verify(sig, data, padding, hash_algorithm) + + elif self.scheme in ["ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]: + key = cast(EllipticCurvePublicKey, self._from_pem()) + hash_name = f"sha{self.scheme[-3:]}" + hash_algorithm = self._get_hash_algorithm(hash_name) + signature_algorithm = ECDSA(hash_algorithm) + key.verify(sig, data, signature_algorithm) + + elif self.scheme in ["ed25519"]: + public_bytes = bytes.fromhex(self.keyval["public"]) + key = Ed25519PublicKey.from_public_bytes(public_bytes) + key.verify(sig, data) + + else: + raise ValueError(f"unknown scheme '{self.scheme}'") + + # Workaround for 'except (SignatureMismatch, InvalidSignature)' to + # conditionally evaluate the optional 'InvalidSignature': + except Exception as e: + if isinstance(e, SignatureMismatch) or ( + not CRYPTO_IMPORT_ERROR and isinstance(e, InvalidSignature) ): raise exceptions.UnverifiedSignatureError( f"Failed to verify signature by {self.keyid}" - ) - except ( - exceptions.CryptoError, - exceptions.FormatError, - exceptions.UnsupportedAlgorithmError, - exceptions.UnsupportedLibraryError, - ) as e: + ) from e + logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) raise exceptions.VerificationError( f"Unknown failure to verify signature by {self.keyid}" From 73f84e26b5d64d4d11b1cb1589258d46e1430ed7 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 31 May 2023 15:23:39 +0200 Subject: [PATCH 2/3] key: re-structure SlibKey.verify_signature Re-structure try/except block according to @jku's review comment, for better readability. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 137 +++++++++++++++++++------------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index 3dbf2260..9efbc05d 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -3,11 +3,15 @@ from abc import ABCMeta, abstractmethod from typing import Any, Dict, Optional, Tuple, Type, cast -from securesystemslib import exceptions from securesystemslib._vendor.ed25519.ed25519 import ( SignatureMismatch, checkvalid, ) +from securesystemslib.exceptions import ( + UnsupportedLibraryError, + UnverifiedSignatureError, + VerificationError, +) from securesystemslib.signer._signature import Signature CRYPTO_IMPORT_ERROR = None @@ -250,65 +254,84 @@ def _get_rsa_padding( return padding def verify_signature(self, signature: Signature, data: bytes) -> None: - try: - if signature.keyid != self.keyid: - raise ValueError( - f"keyid mismatch: 'key id: {self.keyid}" - f" != signature keyid: {signature.keyid}'" + if signature.keyid != self.keyid: + raise VerificationError from ValueError( + f"keyid mismatch: 'key id: {self.keyid}" + f" != signature keyid: {signature.keyid}'" + ) + + sig = bytes.fromhex(signature.signature) + + if CRYPTO_IMPORT_ERROR: + try: + if self.scheme != "ed25519": + raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) + + public_bytes = bytes.fromhex(self.keyval["public"]) + checkvalid(sig, data, public_bytes) + + except SignatureMismatch as e: + raise UnverifiedSignatureError( + f"Failed to verify signature by {self.keyid}" + ) from e + + except Exception as e: + logger.info( + "Key %s failed to verify sig: %s", self.keyid, str(e) ) - sig = bytes.fromhex(signature.signature) + raise VerificationError( + f"Unknown failure to verify signature by {self.keyid}" + ) from e - if CRYPTO_IMPORT_ERROR: - if self.scheme == "ed25519": + else: + try: + key: PublicKeyTypes + if self.scheme in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + key = cast(RSAPublicKey, self._from_pem()) + padding_name, hash_name = self.scheme.split("-")[1:] + hash_algorithm = self._get_hash_algorithm(hash_name) + padding = self._get_rsa_padding( + padding_name, hash_algorithm + ) + key.verify(sig, data, padding, hash_algorithm) + + elif self.scheme in [ + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + ]: + key = cast(EllipticCurvePublicKey, self._from_pem()) + hash_name = f"sha{self.scheme[-3:]}" + hash_algorithm = self._get_hash_algorithm(hash_name) + signature_algorithm = ECDSA(hash_algorithm) + key.verify(sig, data, signature_algorithm) + + elif self.scheme in ["ed25519"]: public_bytes = bytes.fromhex(self.keyval["public"]) - checkvalid(sig, data, public_bytes) - return - - raise exceptions.UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - - key: PublicKeyTypes - if self.scheme in [ - "rsassa-pss-sha224", - "rsassa-pss-sha256", - "rsassa-pss-sha384", - "rsassa-pss-sha512", - "rsa-pkcs1v15-sha224", - "rsa-pkcs1v15-sha256", - "rsa-pkcs1v15-sha384", - "rsa-pkcs1v15-sha512", - ]: - key = cast(RSAPublicKey, self._from_pem()) - padding_name, hash_name = self.scheme.split("-")[1:] - hash_algorithm = self._get_hash_algorithm(hash_name) - padding = self._get_rsa_padding(padding_name, hash_algorithm) - key.verify(sig, data, padding, hash_algorithm) - - elif self.scheme in ["ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]: - key = cast(EllipticCurvePublicKey, self._from_pem()) - hash_name = f"sha{self.scheme[-3:]}" - hash_algorithm = self._get_hash_algorithm(hash_name) - signature_algorithm = ECDSA(hash_algorithm) - key.verify(sig, data, signature_algorithm) - - elif self.scheme in ["ed25519"]: - public_bytes = bytes.fromhex(self.keyval["public"]) - key = Ed25519PublicKey.from_public_bytes(public_bytes) - key.verify(sig, data) - - else: - raise ValueError(f"unknown scheme '{self.scheme}'") - - # Workaround for 'except (SignatureMismatch, InvalidSignature)' to - # conditionally evaluate the optional 'InvalidSignature': - except Exception as e: - if isinstance(e, SignatureMismatch) or ( - not CRYPTO_IMPORT_ERROR and isinstance(e, InvalidSignature) - ): - raise exceptions.UnverifiedSignatureError( + key = Ed25519PublicKey.from_public_bytes(public_bytes) + key.verify(sig, data) + + else: + raise ValueError(f"unknown scheme '{self.scheme}'") + + # the actual switch for cryptography calls goes here + except InvalidSignature as e: + raise UnverifiedSignatureError( f"Failed to verify signature by {self.keyid}" ) from e - logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) - raise exceptions.VerificationError( - f"Unknown failure to verify signature by {self.keyid}" - ) from e + except Exception as e: + logger.info( + "Key %s failed to verify sig: %s", self.keyid, str(e) + ) + raise VerificationError( + f"Unknown failure to verify signature by {self.keyid}" + ) from e From 6e7a71ec065c2d36c7b858657851d5b6962b37b8 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 1 Jun 2023 10:40:51 +0200 Subject: [PATCH 3/3] key: re-structure SlibKey.verify_signature v2 Further re-structure try/except block according to @jku's review comment, for better readability. Move "verify with pyca/crypto" and "verify with vendored ed25519" to separate helpers. Signed-off-by: Lukas Puehringer --- securesystemslib/signer/_key.py | 144 ++++++++++++++++---------------- 1 file changed, 70 insertions(+), 74 deletions(-) diff --git a/securesystemslib/signer/_key.py b/securesystemslib/signer/_key.py index 9efbc05d..7863b886 100644 --- a/securesystemslib/signer/_key.py +++ b/securesystemslib/signer/_key.py @@ -253,85 +253,81 @@ def _get_rsa_padding( return padding + def _verify_ed25519_fallback(self, signature: bytes, data: bytes) -> None: + """Helper to verify ed25519 sig if pyca/cryptography is unavailable.""" + try: + public_bytes = bytes.fromhex(self.keyval["public"]) + checkvalid(signature, data, public_bytes) + + except SignatureMismatch as e: + raise UnverifiedSignatureError from e + + def _verify(self, signature: bytes, data: bytes) -> None: + """Helper to verify signature using pyca/cryptography (default).""" + try: + key: PublicKeyTypes + if self.scheme in [ + "rsassa-pss-sha224", + "rsassa-pss-sha256", + "rsassa-pss-sha384", + "rsassa-pss-sha512", + "rsa-pkcs1v15-sha224", + "rsa-pkcs1v15-sha256", + "rsa-pkcs1v15-sha384", + "rsa-pkcs1v15-sha512", + ]: + key = cast(RSAPublicKey, self._from_pem()) + padding_name, hash_name = self.scheme.split("-")[1:] + hash_algorithm = self._get_hash_algorithm(hash_name) + padding = self._get_rsa_padding(padding_name, hash_algorithm) + key.verify(signature, data, padding, hash_algorithm) + + elif self.scheme in [ + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + ]: + key = cast(EllipticCurvePublicKey, self._from_pem()) + hash_name = f"sha{self.scheme[-3:]}" + hash_algorithm = self._get_hash_algorithm(hash_name) + signature_algorithm = ECDSA(hash_algorithm) + key.verify(signature, data, signature_algorithm) + + elif self.scheme in ["ed25519"]: + public_bytes = bytes.fromhex(self.keyval["public"]) + key = Ed25519PublicKey.from_public_bytes(public_bytes) + key.verify(signature, data) + + else: + raise ValueError(f"unknown scheme '{self.scheme}'") + + except InvalidSignature as e: + raise UnverifiedSignatureError from e + def verify_signature(self, signature: Signature, data: bytes) -> None: - if signature.keyid != self.keyid: - raise VerificationError from ValueError( - f"keyid mismatch: 'key id: {self.keyid}" - f" != signature keyid: {signature.keyid}'" - ) + try: + if signature.keyid != self.keyid: + raise ValueError( + f"keyid mismatch: 'key id: {self.keyid}" + f" != signature keyid: {signature.keyid}'" + ) - sig = bytes.fromhex(signature.signature) + signature_bytes = bytes.fromhex(signature.signature) - if CRYPTO_IMPORT_ERROR: - try: + if CRYPTO_IMPORT_ERROR: if self.scheme != "ed25519": raise UnsupportedLibraryError(CRYPTO_IMPORT_ERROR) - public_bytes = bytes.fromhex(self.keyval["public"]) - checkvalid(sig, data, public_bytes) + return self._verify_ed25519_fallback(signature_bytes, data) - except SignatureMismatch as e: - raise UnverifiedSignatureError( - f"Failed to verify signature by {self.keyid}" - ) from e + return self._verify(signature_bytes, data) - except Exception as e: - logger.info( - "Key %s failed to verify sig: %s", self.keyid, str(e) - ) - raise VerificationError( - f"Unknown failure to verify signature by {self.keyid}" - ) from e - - else: - try: - key: PublicKeyTypes - if self.scheme in [ - "rsassa-pss-sha224", - "rsassa-pss-sha256", - "rsassa-pss-sha384", - "rsassa-pss-sha512", - "rsa-pkcs1v15-sha224", - "rsa-pkcs1v15-sha256", - "rsa-pkcs1v15-sha384", - "rsa-pkcs1v15-sha512", - ]: - key = cast(RSAPublicKey, self._from_pem()) - padding_name, hash_name = self.scheme.split("-")[1:] - hash_algorithm = self._get_hash_algorithm(hash_name) - padding = self._get_rsa_padding( - padding_name, hash_algorithm - ) - key.verify(sig, data, padding, hash_algorithm) - - elif self.scheme in [ - "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", - ]: - key = cast(EllipticCurvePublicKey, self._from_pem()) - hash_name = f"sha{self.scheme[-3:]}" - hash_algorithm = self._get_hash_algorithm(hash_name) - signature_algorithm = ECDSA(hash_algorithm) - key.verify(sig, data, signature_algorithm) - - elif self.scheme in ["ed25519"]: - public_bytes = bytes.fromhex(self.keyval["public"]) - key = Ed25519PublicKey.from_public_bytes(public_bytes) - key.verify(sig, data) - - else: - raise ValueError(f"unknown scheme '{self.scheme}'") - - # the actual switch for cryptography calls goes here - except InvalidSignature as e: - raise UnverifiedSignatureError( - f"Failed to verify signature by {self.keyid}" - ) from e - - except Exception as e: - logger.info( - "Key %s failed to verify sig: %s", self.keyid, str(e) - ) - raise VerificationError( - f"Unknown failure to verify signature by {self.keyid}" - ) from e + except UnverifiedSignatureError as e: + raise UnverifiedSignatureError( + f"Failed to verify signature by {self.keyid}" + ) from e + + except Exception as e: + logger.info("Key %s failed to verify sig: %s", self.keyid, e) + raise VerificationError( + f"Unknown failure to verify signature by {self.keyid}" + ) from e