-
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
signer: refactor SSlibSigner #590
Conversation
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]>
62d0c93
to
ad64dcd
Compare
Just went through all legacy sign/verify tests, of which ~98% assert FormatError for bad function arguments. Most of them are obsolete or different now, given the class-based design and use of mypy. Still, we could probably better test error scenarios depending on our decisions in #468 and #595 (I'll make a note in those issues). Apart from that, basic signature verification testing is already fully covered by new signer API tests, using pre-generated signatures (see #585 (comment)). Similar sign -> verify tests already exist for all key types, but not all schemes. I can change the latter, and maybe port the "auto rsa pss salt length"-test from #436. |
Also moves error tests to separate function. Signed-off-by: Lukas Puehringer <[email protected]>
87537ac
to
82b0b09
Compare
This allows us to add new test cases to the same table. Note: used the following script to patch the tests ``` from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from cryptography.hazmat.primitives.hashes import SHA384 from securesystemslib.interface import ( import_ecdsa_privatekey_from_file, import_ed25519_privatekey_from_file, import_rsa_privatekey_from_file, ) from securesystemslib.signer import SSlibSigner rsa_priv = import_rsa_privatekey_from_file("tests/data/keystore/rsa_key", "password") ed25519_priv = import_ed25519_privatekey_from_file("tests/data/keystore/ed25519_key", "password") ecdsa_priv = import_ecdsa_privatekey_from_file("tests/data/keystore/ecdsa_key", "password") ed25519_keyid = ed25519_priv["keyid"] ed25519_pub = ed25519_priv["keyval"]["public"] rsa_keyid = rsa_priv["keyid"] rsa_pub = repr(rsa_priv["keyval"]["public"]).strip("'") ecdsa_keyid = ecdsa_priv["keyid"] ecdsa_pub = repr(ecdsa_priv["keyval"]["public"]).strip("'") def signature(key, scheme): key["scheme"] = scheme signer = SSlibSigner(key) # Hack requires secure-systems-lab#590 and disabling supported-scheme-check in ECDSASigner if scheme == "ecdsa-sha2-nistp384": signer._crypto_signer._signature_algorithm = ECDSA(SHA384()) signature = signer.sign(b"DATA") return signature.signature print( f""" ed25519_keyid = ( "{ed25519_keyid}" ) ed25519_pub = ( "{ed25519_pub}" ) rsa_keyid = ( "{rsa_keyid}" ) rsa_pub = "{rsa_pub}" ecdsa_keyid = ( "{ecdsa_keyid}" ) ecdsa_pub = "{ecdsa_pub}" key_sig_data = [ ( ed25519_keyid, "ed25519", "ed25519", ed25519_pub, "{signature(ed25519_priv, "ed25519")}", ), ( rsa_keyid, "rsa", "rsassa-pss-sha224", rsa_pub, "{signature(rsa_priv, "rsassa-pss-sha224")}", ), ( rsa_keyid, "rsa", "rsassa-pss-sha256", rsa_pub, "{signature(rsa_priv, "rsassa-pss-sha256")}", ), ( rsa_keyid, "rsa", "rsassa-pss-sha384", rsa_pub, "{signature(rsa_priv, "rsassa-pss-sha384")}", ), ( rsa_keyid, "rsa", "rsassa-pss-sha512", rsa_pub, "{signature(rsa_priv, "rsassa-pss-sha512")}", ), ( rsa_keyid, "rsa", "rsa-pkcs1v15-sha224", rsa_pub, "{signature(rsa_priv, "rsa-pkcs1v15-sha224")}", ), ( rsa_keyid, "rsa", "rsa-pkcs1v15-sha256", rsa_pub, "{signature(rsa_priv, "rsa-pkcs1v15-sha256")}", ), ( rsa_keyid, "rsa", "rsa-pkcs1v15-sha384", rsa_pub, "{signature(rsa_priv, "rsa-pkcs1v15-sha384")}", ), ( rsa_keyid, "rsa", "rsa-pkcs1v15-sha512", rsa_pub, "{signature(rsa_priv, "rsa-pkcs1v15-sha512")}", ), ( ecdsa_keyid, "ecdsa", "ecdsa-sha2-nistp256", ecdsa_pub, "{signature(ecdsa_priv, "ecdsa-sha2-nistp256")}", ), ( ecdsa_keyid, "ecdsa", "ecdsa-sha2-nistp384", ecdsa_pub, "{signature(ecdsa_priv, "ecdsa-sha2-nistp384")}", ), ( ecdsa_keyid, "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp256", ecdsa_pub, "{signature(ecdsa_priv, "ecdsa-sha2-nistp256")}", ), ( ecdsa_keyid, "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp384", ecdsa_pub, "{signature(ecdsa_priv, "ecdsa-sha2-nistp384")}", ), ] """ ) ``` Signed-off-by: Lukas Puehringer <[email protected]>
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]>
@@ -225,5 +261,146 @@ def sign(self, payload: bytes) -> Signature: | |||
securesystemslib.exceptions.UnsupportedAlgorithmError: | |||
Signing errors. | |||
""" |
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.
TODO: Update exceptions in docstring
Add optinal signer arg to runlib's run and record methods. NOTE: needs not yet public CryptoSigner from securesy secure-systems-lab/securesystemslib#590 TODO: - Add deprecation warnings to helper (<key> argument is deprecated and will be removed in 3.0.0 - Add tests, also with non-default signers - Support verifier (Key) argument in in_toto_record_stop if passed: use verifier.keyid to load preliminary link metadata pass verifier.to_dict() to Metadata.verify_signature elif signer is a CryptoSigner: use signer.public_key.keyid to load ... pass signer.public_key.to_dict to ... elif signer is a GPGSigner do gpg tricks (glob and grab signature keyid) to load file and export key from keychain to get verifier Future work (follup-up) - Add something like Metadata.verify(public_key: Key), if necessary - add deprecation warning to Metadata.verify_signature( - update record_stop and other places where verify_signature is used Signed-off-by: Lukas Puehringer <[email protected]>
IMPORTANT: Requires unlreleased securesystemslib code from secure-systems-lab/securesystemslib#590 Adds optional signer (Signer) arg to runlib's run/record functions, as alternative way of signing resulting link metadata, instead of using signing_key, gpg_keyid, or use_default_gpg. Closes in-toto#532 **Addtional notes:** In in_toto_record_stop, the public_key (Key) field of the signer is used to verify the preliminary link metadata file. The field is not yet part of the interface, although all signer implementations in secureystemslib have it: secure-systems-lab/securesystemslib#605 The patch aims to be minimally invasive, and thus barely refactors any of the existing signing argument handling in the relevant functions. Although, it was tempting to simplify the code, it turned out harder than thought, and therefor not worth the effort, given that these arguments are bound to deprecation. This is patch is part of a series of patches to prepare for the planned removal of securesystemslib legacy interfaces, see secure-systems-lab/securesystemslib#604 for details. **TODO (follow-up)** - add deprecation warning for legacy key args and legacy gpg formats (only show on API use, not on CLI use!) - add Metadata.verify(public_key: Key), if necessary (maybe we can just do this in runlib/verifylib), and use in in_toto_record_stop instead of passing `signer.public_key.to_dict()` to `Metadata.verify_signature(...)` - prepare for ssslib legacy interface removal in other parts of in_toto: - in-toto-run, in-toto-record (in-toto#533) - verifylib / in-toto-verify - in-toto-sign - Metadata API / docs - ... 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 the core idea makes sense: separate two responsibilities:
- loading the private keys from storage (handled by SSLibSigner at the moment, could change in future)
- actual cryptography API usage
Code looks good. Two questions:
- is it not confusing that CryptoSigner claims to be a Signer? (more in a code comment)
- should we maybe move SSlibSigner out of _signer.py? The interface definition might be easier to understand without a screenful of crypto imports
return self._crypto_signer.sign(payload) | ||
|
||
|
||
class CryptoSigner(Signer, metaclass=ABCMeta): |
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.
What's the advantage of deriving here? You say that the individual CryptoSigners could be used as Signers directly but if they don't implement from_priv_key_uri()
I'm not sure I agree. The class hierarchy then has a lot of "Signers" but only some can be used as documented: others will throw a RuntimeError
I think the division of responsibility makes sense but could this just be a clearly internal class that isn't a Signer?
@@ -3,15 +3,49 @@ | |||
import logging | |||
import os | |||
from abc import ABCMeta, abstractmethod | |||
from typing import Any, Callable, Dict, Optional, Type | |||
from typing import Any, Callable, Dict, Optional, Type, cast | |||
from urllib import parse | |||
|
|||
import securesystemslib.keys as sslib_keys |
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.
As a note (unrealted to this PR), I was wondering what was still keeping this import in:
We call decrypt_key()
for the encrypted private key use case (there's also a matching securesystemslib.keys.encrypt_key()
call).
I'm sure cryptography has something we could use instead?
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'm sure cryptography has something we could use instead?
Yes, it has, and #604 uses it consistently for all 3 supported key types to import encrypted private key files from a standard PEM format simply by...
private_key = load_pem_private_key(private_pem, secret) |
#604 does not yet provide API to serialise and dump corresponding private keys to disk but that would be extremely easy to to add using CryptoSigner.private_key.private_bytes
as described here.
securesystemslib
really shouldn't implement any more complex encryption or decryption logic than this.
Agreed, when looking at this PR alone However, I did intend for
Yes, we should. I had a version of this PR, where I did move it out, but then reconsidered to keep the diff simpler and also due to some circular import hiccups, which surely can be fixed. |
superseded by #604 |
edit: ticketized and removed TODOs below (#593, #594, #592)
This patch refactors the SSlibSigner signing implementation, akin to #585, which refactored signature verification (see PR for details about legacy code and rationale).
Unlike, #585 this patch does not only condense and move the code fo singing, but creates a new hierarchy of signers to achieve tw additional goals:
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.
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.
See full diff including the removal of related legacy code for how much more lightweight the code becomes:
194 additions and 1,264 deletions
(!)