Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstract methods added to Key for signing scheme dissection #837

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions securesystemslib/signer/_azure_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, az_key_uri: str, public_key: Key):
self.signature_algorithm = self._get_signature_algorithm(
public_key,
)
self.hash_algorithm = self._get_hash_algorithm(public_key)
self.hash_algorithm = public_key.get_hash_algorithm_str()
except UnsupportedKeyType as e:
logger.info(
"Key %s has unsupported key type or unsupported elliptic curve"
Expand Down Expand Up @@ -144,23 +144,6 @@ def _get_signature_algorithm(public_key: Key) -> "SignatureAlgorithm":

raise UnsupportedKeyType("Unsupported curve supplied by key")

@staticmethod
def _get_hash_algorithm(public_key: "Key") -> str:
"""Return the hash algorithm used by the public key"""
# Format is "ecdsa-sha2-nistp256"
comps = public_key.scheme.split("-")
if len(comps) != 3: # noqa: PLR2004
raise UnsupportedKeyType("Invalid scheme found")

if comps[2] == "nistp256":
return "sha256"
if comps[2] == "nistp384":
return "sha384"
if comps[2] == "nistp521":
return "sha512"

raise UnsupportedKeyType("Unsupported curve supplied by key")

@staticmethod
def _get_keytype_and_scheme(crv: str) -> Tuple[str, str]:
if crv == KeyCurveName.p_256:
Expand Down
35 changes: 3 additions & 32 deletions securesystemslib/signer/_crypto_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from securesystemslib.signer._signature import Signature
from securesystemslib.signer._signer import SecretsHandler, Signer

# ruff: noqa: F401
CRYPTO_IMPORT_ERROR = None
try:
from cryptography.hazmat.primitives.asymmetric.ec import (
Expand Down Expand Up @@ -77,35 +78,6 @@ class _NoSignArgs:
_ECDSA_KEYTYPES = ["ecdsa", "ecdsa-sha2-nistp256"]


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


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.DIGEST_LENGTH)

if name == "pkcs1v15":
padding = PKCS1v15()

return padding


class CryptoSigner(Signer):
"""PYCA/cryptography Signer implementations.

Expand Down Expand Up @@ -157,9 +129,8 @@ def __init__(
if not isinstance(private_key, RSAPrivateKey):
raise ValueError(f"invalid rsa key: {type(private_key)}")

padding_name, hash_name = public_key.scheme.split("-")[1:]
hash_algo = _get_hash_algorithm(hash_name)
padding = _get_rsa_padding(padding_name, hash_algo)
hash_algo = public_key.get_hash_algorithm()
padding = public_key.get_padding_name(hash_algo)
self._sign_args = _RSASignArgs(padding, hash_algo)
self._private_key = private_key

Expand Down
29 changes: 1 addition & 28 deletions securesystemslib/signer/_gcp_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(self, gcp_keyid: str, public_key: Key):
if GCP_IMPORT_ERROR:
raise exceptions.UnsupportedLibraryError(GCP_IMPORT_ERROR)

self.hash_algorithm = self._get_hash_algorithm(public_key)
self.hash_algorithm = public_key.get_hash_algorithm_str()
self.gcp_keyid = gcp_keyid
self._public_key = public_key
self.client = kms.KeyManagementServiceClient()
Expand Down Expand Up @@ -155,33 +155,6 @@ def _get_keytype_and_scheme(algorithm: int) -> Tuple[str, str]:
}
return keytypes_and_schemes[algorithm]

@staticmethod
def _get_hash_algorithm(public_key: Key) -> str:
"""Helper function to return payload hash algorithm used for this key"""

# TODO: This could be a public abstract method on Key so that GCPSigner
# would not be tied to a specific Key implementation -- not all keys
# have a pre hash algorithm though.
if public_key.keytype == "rsa":
# hash algorithm is encoded as last scheme portion
algo = public_key.scheme.split("-")[-1]
elif public_key.keytype in [
"ecdsa",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
]:
# nistp256 uses sha-256, nistp384 uses sha-384
bits = public_key.scheme.split("-nistp")[-1]
algo = f"sha{bits}"
else:
raise exceptions.UnsupportedAlgorithmError(
f"Unsupported key type {public_key.keytype} in key {public_key.keyid}"
)

# trigger UnsupportedAlgorithm if appropriate
_ = sslib_hash.digest(algo)
return algo

def sign(self, payload: bytes) -> Signature:
"""Signs payload with Google Cloud KMS.

Expand Down
12 changes: 12 additions & 0 deletions securesystemslib/signer/_gpg_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ def verify_signature(self, signature: Signature, data: bytes) -> None:
f"Unknown failure to verify signature by {self.keyid}"
) from e

def get_hash_algorithm_str(self) -> None:
raise NotImplementedError

def get_hash_algorithm(self) -> None:
raise NotImplementedError

def get_padding_name_str(self) -> None:
raise NotImplementedError

def get_padding_name(self, hash_algorithm: None) -> None:
raise NotImplementedError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These stubs are no longer needed, right?



class GPGSigner(Signer):
"""OpenPGP Signer
Expand Down
126 changes: 94 additions & 32 deletions securesystemslib/signer/_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from abc import ABCMeta, abstractmethod
from typing import Any, Dict, Optional, Tuple, Type, cast

import securesystemslib.hash as sslib_hash
from securesystemslib._vendor.ed25519.ed25519 import (
SignatureMismatch,
checkvalid,
)
from securesystemslib.exceptions import (
UnsupportedAlgorithmError,
UnsupportedLibraryError,
UnverifiedSignatureError,
VerificationError,
Expand Down Expand Up @@ -56,6 +58,11 @@

logger = logging.getLogger(__name__)


class UnsupportedKeyType(Exception): # noqa: N818
pass
Comment on lines +62 to +63
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be marked private (_UnsupportedKeyType) since I don't think we intend to leak it outside... but that's a nit



# NOTE Key dispatch table is defined here so it's usable by Key,
# but is populated in __init__.py (and can be appended by users).
KEY_FOR_TYPE_AND_SCHEME: Dict[Tuple[str, str], Type] = {}
Expand Down Expand Up @@ -196,6 +203,32 @@ def verify_signature(self, signature: Signature, data: bytes) -> None:
"""
raise NotImplementedError

@abstractmethod
def get_hash_algorithm_str(self) -> Any:
"""Returns payload hash algorithm used for this key as a str

Raises:
UnsupportedAlgorithmError: if key type not suported
"""
raise NotImplementedError

@abstractmethod
def get_hash_algorithm(self) -> Any:
"""Returns payload hash algorithm used for this key as a HashAlgorithm"""
raise NotImplementedError

@abstractmethod
def get_padding_name_str(self) -> Any:
"""Return payload padding name used for this key as a str"""

raise NotImplementedError

@abstractmethod
def get_padding_name(self, hash_algorithm: Any) -> Any:
"""Return payload padding name used for this key as a AsymmetricPadding"""

raise NotImplementedError

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really see a benefit of adding these to the Key interface. The only subclass, which implements them, is SSlibKey. Why don't make these SSlibKey-only methods?

Copy link
Contributor Author

@L77H L77H Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _azure_signer.py and _gcp_signer.py both AzureSigner and GCPSigner take in a public key which is a Key object, on which they use the get hash method. Can this be changed to SSlibKey?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Good question. It looks like all Signer implementations take Keys, even though they can only handle specific Key implementations. That's probably okay. And I definitely don't want to disrupt anyone, by changing it.

But adding new behaviour to an abstract base class, which is specific to one subclass only, and raising NotImplementedError everywhere else, does not feel right.

Would it be okay to add regular functions, e.g. to _utils?

Thoughts, @jku?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit I didn't look at the specific uses in AzureSigner and GCPSigner yet but I would say it's fine for them to handle SSlibKey only -- that sounds correct to me, what else could the key be?

Whether that should be done in practice by type checking inside the AzureSigner and GCPSigner functions or by changing some public argument types I can't say without having a closer look...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my last commit the methods are now SSlibKey-only methods. I added the stricter type checking on GCPSigner and AzureSigner and some of their methods including a method from the Signer class. It seems to pass all the tests, but a more detailed review by @lukpueh and @jku would be advised.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! If the change isn't likely to bother any user, then I'd go with it. I'll give a detailed review after #836 is merged.


class SSlibKey(Key):
"""Key implementation for RSA, Ed25519, ECDSA keys"""
Expand Down Expand Up @@ -301,35 +334,6 @@ def from_crypto(

return SSlibKey(keyid, keytype, scheme, keyval)

@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_ed25519_fallback(self, signature: bytes, data: bytes) -> None:
"""Helper to verify ed25519 sig if pyca/cryptography is unavailable."""
try:
Expand Down Expand Up @@ -364,9 +368,8 @@ def _validate_curve(key, curve):
]:
key = cast(RSAPublicKey, self._crypto_key())
_validate_type(key, RSAPublicKey)
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)
hash_algorithm = self.get_hash_algorithm()
padding = self.get_padding_name(hash_algorithm)
key.verify(signature, data, padding, hash_algorithm)

elif (
Expand Down Expand Up @@ -428,3 +431,62 @@ def verify_signature(self, signature: Signature, data: bytes) -> None:
raise VerificationError(
f"Unknown failure to verify signature by {self.keyid}"
) from e

def get_hash_algorithm_str(self) -> str:
# key scheme should always be of format xxx-xxx-xxx
comps = self.scheme.split("-")
if len(comps) != 3: # noqa: PLR2004
raise UnsupportedKeyType("Invalid scheme found")

if self.keytype == "rsa":
# hash algorithm is encoded as last scheme portion
hash_algo = self.scheme.split("-")[-1]
elif self.keytype in [
"ecdsa",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
]:
# nistp256 uses sha-256, nistp384 uses sha-384
bits = self.scheme.split("-nistp")[-1]
hash_algo = f"sha{bits}"
else:
raise UnsupportedAlgorithmError(
f"Unsupported key type {self.keytype} in key {self.keyid}"
)

# trigger UnsupportedAlgorithm if appropriate
_ = sslib_hash.digest(hash_algo)

return hash_algo

def get_hash_algorithm(self) -> "HashAlgorithm":
name = self.get_hash_algorithm_str()
algorithm: HashAlgorithm
if name == "sha224":
algorithm = SHA224()
if name == "sha256":
algorithm = SHA256()
if name == "sha384":
algorithm = SHA384()
if name == "sha512":
algorithm = SHA512()

return algorithm

def get_padding_name_str(self) -> str:
padding_name = self.scheme.split("-")[1]

return padding_name

def get_padding_name(
self, hash_algorithm: "HashAlgorithm"
) -> "AsymmetricPadding":
name = self.get_padding_name_str()
padding: AsymmetricPadding
if name == "pss":
padding = PSS(mgf=MGF1(hash_algorithm), salt_length=PSS.AUTO)

if name == "pkcs1v15":
padding = PKCS1v15()

return padding
12 changes: 12 additions & 0 deletions securesystemslib/signer/_sigstore_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ def verify_signature(self, signature: Signature, data: bytes) -> None:
f"Unknown failure to verify signature by {self.keyid}"
) from e

def get_hash_algorithm_str(self) -> None:
raise NotImplementedError

def get_hash_algorithm(self) -> None:
raise NotImplementedError

def get_padding_name_str(self) -> None:
raise NotImplementedError

def get_padding_name(self, hash_algorithm: None) -> None:
raise NotImplementedError


class SigstoreSigner(Signer):
"""Sigstore signer.
Expand Down
12 changes: 12 additions & 0 deletions securesystemslib/signer/_spx_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ def verify_signature(self, signature: Signature, data: bytes) -> None:
f"Failed to verify signature by {self.keyid}"
)

def get_hash_algorithm_str(self) -> None:
raise NotImplementedError

def get_hash_algorithm(self) -> None:
raise NotImplementedError

def get_padding_name_str(self) -> None:
raise NotImplementedError

def get_padding_name(self, hash_algorithm: None) -> None:
raise NotImplementedError


class SpxSigner(Signer):
"""SPHINCS+ signer.
Expand Down
Loading