-
Notifications
You must be signed in to change notification settings - Fork 50
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
key: refactor SSlibKey.verify_signature #585
key: refactor SSlibKey.verify_signature #585
Conversation
92c3809
to
cb09097
Compare
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 <[email protected]>
cb09097
to
3247ed7
Compare
wow. Haven't read the code yet but that looks really nice in comparison. |
This patch refactors the SSlibSigner signing implementation, akin to secure-systems-lab#585, which refactored signature verification (see PR for details about legacy code and rationale). Unlike, secure-systems-lab#585 this patch does not only condense and move the code for singing, but creates a new hierarchy of signers to achieve two additional goals: 1. Provide tiny rsa, ecdsa and ed25519 pyca/cryptography Signer implementations, which are independent of private key serialization formats, above all, of the proprietary legacy keydict format. This is particularly interesting, when refactoring existing or designing new key generation or import interfaces, where it would be annoying to move back and forth over the legacy keydict. 2. Preserve SSlibSigner including its internal legacy keydict data structure. SSlibSigner is and remains a backwards-compatibility crutch. Breaking its existing users to make it a little less awkward would defeat that purpose. And even though the Signer API doc says that the internal data structure is not part of the public API, users may rely on it (python-tuf actually does so at least in tests and demos). To achieve these goals, SSlibSigner becomes a container for the newly added CryptoSigner class, whose implementations can also be used as independent Signers, and above all created or imported, with very few lines of pyca/cryptography code. **Caveat:** Latest python-tuf tests pass against this patch, except for one, which expects a keydict deserialization failure in `sign`, which now happens in `__init__` initialization time. This seems feasible to fix in python-tuf. Also note that private key format errors are now ValueErrors and no longer unreliably either FormatErrors or sometimes UnsupportedAlgorithmErrors. **Future work (will ticketize):** - Signing schemes strings should not be hardcoded all over the place but defined once in constants for all of securesystemslib. - There is some duplicate code for scheme string dissection and algorithm selection, which could be unified for all signers and public keys. - (bonus) secure-systems-lab#585 considered creating separate RSAKey, ECDSAKey, ED25519Key classes, but ended up putting everything into SSlibKey. Now that we have separate signers for each of these key types, which have a field for the corresponding public key object, it might make sense to reconsider this separation. This would give us a more robust data model, where e.g. allowed signing schemes are only validated once in the public key constructor and are thus validated implicitly in the signer constructor. Signed-off-by: Lukas Puehringer <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks nice. Approving as is but commented one possible change.
I have a question about testing though. How well is signer.Key.verify_signature()
getting tested? Obviously we're not adding new API here but... before this it wasn't really that critical that signer.Key.verify_signature()
had good coverage as the implementation was known to be a wrapper over keys.verify_signature()
with some testing of its own. Now the situation is a bit different.
So... how do you feel about the test coverage for the "new" implementation? Is it good enough?
Oh, I forgot to mention that I have already added more tests for So, I do feel quite confident. |
Re-structure try/except block according to @jku's review comment, for better readability. Signed-off-by: Lukas Puehringer <[email protected]>
21afae6
to
8160f6b
Compare
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 <[email protected]>
8160f6b
to
6e7a71e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, feels much easier to understand.
The exceptions still feel a bit clunky (like handling generic Exception) but I think my suggestions are not very actionable... so LGTM
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handling generic Exception is not amazing but I can see how it's useful here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you are right. The question is, what do we actually want to raise as VerificationError?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should UnsupportedLibraryError be re-raised as VerificationError? Was e6529cd a mistake?
if signature.keyid != self.keyid: | ||
raise ValueError( | ||
f"keyid mismatch: 'key id: {self.keyid}" | ||
f" != signature keyid: {signature.keyid}'" | ||
) | ||
except ( | ||
exceptions.CryptoError, | ||
exceptions.FormatError, | ||
exceptions.UnsupportedAlgorithmError, | ||
exceptions.UnsupportedLibraryError, | ||
) as e: | ||
logger.info("Key %s failed to verify sig: %s", self.keyid, str(e)) | ||
raise exceptions.VerificationError( | ||
|
||
signature_bytes = bytes.fromhex(signature.signature) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could all be outside of the try block... raising things just to catch them in the same method seems odd
- just raising a ValueError from this method might be fine: the idea is that it is a programming error to try to verify a signature that does not match the key --
- if you don't want to raise ValueError from this method, you could just raise VerificationError before the try block
That said, doing this does have similarities to how the _verify*
methods raise ValueError etc that are then handled in this method... so maybe it's fine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this could all be outside of the try block... raising things just to catch them in the same method seems odd
- just raising a ValueError from this method might be fine: the idea is that it is a programming error to try to verify a signature that does not match the key --
I added this here to pass python-tuf tests. Previously, the keyid mismatch triggered a FormatError, which was caught and re-raised as VerificationError and asserted for in tests. Without the keyid check, this will try verification and fail as UnverifiedSignatureError.
Obviously, we can change the python-tuf test. Then we should maybe update the Key.verify_signature docs.
- if you don't want to raise ValueError from this method, you could just raise VerificationError before the try block
Yes, I considered that too. But I thought it is semantically more correct to raise a ValueError as VerificationError, and that I could re-use the generic log.info + raise VerificationError block used for everything that is not an UnverifiedSignatureError. But now that you point this out, it does not seem fully correct to re-raise this ValueError as "Unknown failure"
That said, doing this does have similarities to how the
_verify*
methods raise ValueError etc that are then handled in this method... so maybe it's fine
Yes, it is consistent, which does not mean it is right. :) I could also raise a VerificationError in _verify*
, which I don't catch here and just let fall through.
Btw. we use the |
Since secure-systems-lab#585 SSlibKey no longer uses securesystemslib.keys to verify signatures, and thus no longer is tested via `test_*keys`. Good test coverage of the new SSlibKey implementation is already available in test_signer. This PR ports one missing test from: `test_rsa_keys.TestRSA_keys.test_verify_rsa_pss_different_salt_lengths` Used script to create test table entry (requires secure-systems-lab#590): ``` from cryptography.hazmat.primitives.asymmetric.padding import MGF1, PSS from cryptography.hazmat.primitives.hashes import SHA256 from securesystemslib.interface import import_rsa_privatekey_from_file from securesystemslib.signer import SSlibSigner scheme = "rsassa-pss-sha256" rsa_priv = import_rsa_privatekey_from_file( "tests/data/keystore/rsa_key", password="password", scheme=scheme ) signer = SSlibSigner(rsa_priv) signer._crypto_signer._padding = PSS( mgf=MGF1(SHA256()), salt_length=PSS.MAX_LENGTH ) signature = signer.sign(b"DATA") print( f""" # Test sig with max salt length (briefly available in v0.24.0) ( rsa_keyid, "rsa", "{scheme}", rsa_pub, "{signature.signature}", ), """ ) ``` Signed-off-by: Lukas Puehringer <[email protected]>
This patch refactors the SSlibSigner signing implementation, akin to secure-systems-lab#585, which refactored signature verification (see PR for details about legacy code and rationale). Unlike, secure-systems-lab#585 this patch does not only condense and move the code for singing, but creates a new hierarchy of signers to achieve two additional goals: 1. Provide tiny rsa, ecdsa and ed25519 pyca/cryptography Signer implementations, which are independent of private key serialization formats, above all, of the proprietary legacy keydict format. This is particularly interesting, when refactoring existing or designing new key generation or import interfaces, where it would be annoying to move back and forth over the legacy keydict. 2. Preserve SSlibSigner including its internal legacy keydict data structure. SSlibSigner is and remains a backwards-compatibility crutch. Breaking its existing users to make it a little less awkward would defeat that purpose. And even though the Signer API doc says that the internal data structure is not part of the public API, users may rely on it (python-tuf actually does so at least in tests and demos). To achieve these goals, SSlibSigner becomes a container for the newly added CryptoSigner class, whose implementations can also be used as independent Signers, and above all created or imported, with very few lines of pyca/cryptography code. **Caveat:** Latest python-tuf tests pass against this patch, except for one, which expects a keydict deserialization failure in `sign`, which now happens in `__init__` initialization time. This seems feasible to fix in python-tuf. Also note that private key format errors are now ValueErrors and no longer unreliably either FormatErrors or sometimes UnsupportedAlgorithmErrors. **Future work (will ticketize):** - Signing schemes strings should not be hardcoded all over the place but defined once in constants for all of securesystemslib. - There is some duplicate code for scheme string dissection and algorithm selection, which could be unified for all signers and public keys. - (bonus) secure-systems-lab#585 considered creating separate RSAKey, ECDSAKey, ED25519Key classes, but ended up putting everything into SSlibKey. Now that we have separate signers for each of these key types, which have a field for the corresponding public key object, it might make sense to reconsider this separation. This would give us a more robust data model, where e.g. allowed signing schemes are only validated once in the public key constructor and are thus validated implicitly in the signer constructor. Signed-off-by: Lukas Puehringer <[email protected]>
This patch refactors the SSlibSigner signing implementation, akin to secure-systems-lab#585, which refactored signature verification (see PR for details about legacy code and rationale). Unlike, secure-systems-lab#585 this patch does not only condense and move the code for singing, but creates a new hierarchy of signers to achieve two additional goals: 1. Provide tiny rsa, ecdsa and ed25519 pyca/cryptography Signer implementations, which are independent of private key serialization formats, above all, of the proprietary legacy keydict format. This is particularly interesting, when refactoring existing or designing new key generation or import interfaces, where it would be annoying to move back and forth over the legacy keydict. 2. Preserve SSlibSigner including its internal legacy keydict data structure. SSlibSigner is and remains a backwards-compatibility crutch. Breaking its existing users to make it a little less awkward would defeat that purpose. And even though the Signer API doc says that the internal data structure is not part of the public API, users may rely on it (python-tuf actually does so at least in tests and demos). To achieve these goals, SSlibSigner becomes a container for the newly added CryptoSigner class, whose implementations can also be used as independent Signers, and above all created or imported, with very few lines of pyca/cryptography code. **Caveat:** Latest python-tuf tests pass against this patch, except for one, which expects a keydict deserialization failure in `sign`, which now happens in `__init__` initialization time. This seems feasible to fix in python-tuf. Also note that private key format errors are now ValueErrors and no longer unreliably either FormatErrors or sometimes UnsupportedAlgorithmErrors. **Future work (will ticketize):** - Signing schemes strings should not be hardcoded all over the place but defined once in constants for all of securesystemslib. - There is some duplicate code for scheme string dissection and algorithm selection, which could be unified for all signers and public keys. - (bonus) secure-systems-lab#585 considered creating separate RSAKey, ECDSAKey, ED25519Key classes, but ended up putting everything into SSlibKey. Now that we have separate signers for each of these key types, which have a field for the corresponding public key object, it might make sense to reconsider this separation. This would give us a more robust data model, where e.g. allowed signing schemes are only validated once in the public key constructor and are thus validated implicitly in the signer constructor. Signed-off-by: Lukas Puehringer <[email protected]>
EDIT 2023/05/20:
13 additions and 1,010 deletions.
(!)Description of the changes being introduced by the pull request:
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 functionkeys.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.
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.