diff --git a/in_toto/models/_signer.py b/in_toto/models/_signer.py index ce70ce0de..bb8466c68 100644 --- a/in_toto/models/_signer.py +++ b/in_toto/models/_signer.py @@ -15,17 +15,69 @@ See LICENSE for licensing information. - Provides in-toto flavored GPGSigner, GPGSignature and GPGKey. + Provides in-toto flavored Signer implementations """ from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, cast import securesystemslib.gpg.exceptions as gpg_exceptions import securesystemslib.gpg.functions as gpg -from securesystemslib import exceptions -from securesystemslib.signer import Key, SecretsHandler, Signature, Signer +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from securesystemslib import ( + KEY_TYPE_ECDSA, + KEY_TYPE_ED25519, + KEY_TYPE_RSA, + exceptions, +) +from securesystemslib.signer import ( + CryptoSigner, + Key, + SecretsHandler, + Signature, + Signer, + SSlibKey, +) +from securesystemslib.signer._crypto_signer import ( + _ECDSASigner, + _Ed25519Signer, + _RSASigner, +) + + +def load_crypto_signer_from_pkcs8_file( + path: str, password: Optional[bytes] = None +) -> CryptoSigner: + """Internal helper to load CryptoSigner from PKCS8/PEM file.""" + # TODO: coordinate with sslib to not require protected access + # pylint: disable=protected-access + with open(path, "rb") as f: + data = f.read() + + private_key = load_pem_private_key(data, password) + + # TODO: Fix upstream, we don't want to use protected methods + public_key = SSlibKey._from_crypto_public_key( + private_key.public_key(), None, None + ) + + # TODO: Fix upstream, we don't want to use protected classes + if public_key.keytype == KEY_TYPE_RSA: + signer = _RSASigner(public_key, cast(rsa.RSAPrivateKey, private_key)) + elif public_key.keytype == KEY_TYPE_ECDSA: + signer = _ECDSASigner( + public_key, cast(ec.EllipticCurvePrivateKey, private_key) + ) + elif public_key.keytype == KEY_TYPE_ED25519: + signer = _Ed25519Signer( + public_key, cast(ed25519.Ed25519PrivateKey, private_key) + ) + else: # can't be reached + raise ValueError(f"unsupported keytype: {public_key.keytype}") + + return signer class GPGSignature(Signature): diff --git a/tests/models/test_signer.py b/tests/models/test_signer.py index cb6075ed3..c045e40a0 100644 --- a/tests/models/test_signer.py +++ b/tests/models/test_signer.py @@ -21,6 +21,7 @@ """ import unittest +from pathlib import Path from securesystemslib.exceptions import ( UnverifiedSignatureError, @@ -29,7 +30,12 @@ from securesystemslib.gpg.constants import have_gpg from securesystemslib.gpg.functions import export_pubkey -from in_toto.models._signer import GPGKey, GPGSignature, GPGSigner +from in_toto.models._signer import ( + GPGKey, + GPGSignature, + GPGSigner, + load_crypto_signer_from_pkcs8_file, +) from tests.common import GPGKeysMixin, TmpDirMixin @@ -161,3 +167,25 @@ def test_gpg_key_equality(self): key2.keyval = key1.keyval self.assertEqual(key2, key1) + + +class TestCryptoSigner(unittest.TestCase): + """Test helper to load CryptoSigner""" + + def test_load_keys(self): + pems_dir = Path(__file__).parent.parent / "pems" + + for algo in ["rsa", "ecdsa", "ed25519"]: + path = pems_dir / f"{algo}_private_unencrypted.pem" + signer = load_crypto_signer_from_pkcs8_file(path) + self.assertEqual(signer.public_key.keytype, algo) + + path = pems_dir / f"{algo}_private_encrypted.pem" + signer2 = load_crypto_signer_from_pkcs8_file(path, b"hunter2") + self.assertEqual( + signer.public_key.to_dict(), signer2.public_key.to_dict() + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pems/ecdsa_private_encrypted.pem b/tests/pems/ecdsa_private_encrypted.pem new file mode 100644 index 000000000..50536f982 --- /dev/null +++ b/tests/pems/ecdsa_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGwMBsGCSqGSIb3DQEFAzAOBAi7xKwdryb3lAICCAAEgZBz9ttxivvOc6XJKG5j +Ev55zbGqCRSoUn+deGgy/osENhbn4xTOYKRKXGMbfF16t7qvUtX9hHozrGeVIdYg +4R7hFYxgMFlYTTVcN30fPwAV2ePtmFu4vo1/TSLhLxRhv1F3GPLoOSzZxT8FP9oh +Rd9BeAgPPC5RPBJJVThTCXesCV4JWUpY2Wf0DjpFvo3OV4w= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/pems/ecdsa_private_unencrypted.pem b/tests/pems/ecdsa_private_unencrypted.pem new file mode 100644 index 000000000..d7b5a7f73 --- /dev/null +++ b/tests/pems/ecdsa_private_unencrypted.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIP90wnfOXb44C0fH +xubol3Vc2jsIYgBTQq0Oh84d3RWhRANCAARwthJnIUZ4p1Y23l1YVue/o303IcKh +Q0twboZkjEvA3xDwxR0d047EaQOfIFFImkhn+v+gMQJJPB8JiF2iDB4s +-----END PRIVATE KEY----- diff --git a/tests/pems/ed25519_private_encrypted.pem b/tests/pems/ed25519_private_encrypted.pem new file mode 100644 index 000000000..14cfdff46 --- /dev/null +++ b/tests/pems/ed25519_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAi1r8RB+89SSQICCAAw +DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEC+4jGFz/I4eGd/sfUuFVXgEQD6d +idJtTe06bGSHcI66yxwUHolWyiVnnup79tGvv1y6R40P3vvxdA5EThp33HCLEE29 +RAa02JqNkOK8DwzVZw8= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/pems/ed25519_private_unencrypted.pem b/tests/pems/ed25519_private_unencrypted.pem new file mode 100644 index 000000000..d197b99c9 --- /dev/null +++ b/tests/pems/ed25519_private_unencrypted.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGiI3w9x2HZ9UKGi51USN5JN2wtppaYVCRIBTp8ESaj3 +-----END PRIVATE KEY----- diff --git a/tests/pems/rsa_private_encrypted.pem b/tests/pems/rsa_private_encrypted.pem new file mode 100644 index 000000000..166891bc6 --- /dev/null +++ b/tests/pems/rsa_private_encrypted.pem @@ -0,0 +1,29 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIE4TAbBgkqhkiG9w0BBQMwDgQI/DHsRq4QSygCAggABIIEwI3XLWQ170mJ3qYl +miu7ULDvtIFfkp0Yjyvoo5PrwOQQYB5kD4Q4XkqPM3GOidmU0c0AiFtcLuxqD0/X +ivxPy/rpE9IW0uEtzw/8bF/eqyTTs+JZryYDYoNEqrLN8SxA5oMI+SCiz8Q43/y4 +s6XOlaMKH9N+YcIZMSSVG0In/aQ7b5B2jehaB/eKTZmsvjk/7XMEHQ4vtjclswHd +yFbNk/Qh4MAIDJ9SCghwWyQp382ledKzqYPJbngkUu70Inbx0T2trh4wHE9c2/0Q +pymbSPLdPEPeDybt7VYenikQZBI9myyE7PfZN+Mj6HB01ziI1BTwegCtOL0XMYQD +J8aOCf5+qIcbouqvroMKgTYhpivacgzPwiuX+zDZv6FxDwn2Y16nIsrCYssSMCK0 +COLynbXxTtijeHYF2GTymTKQD5caZ4eRV68JLpaPSees4UFGbgxUUmhrRtcO7vt4 +FS4axe7AFOIEnBYEAsrmKb0hfq45jaBp7FQQ2abG+xO2wG6pNvM0omaP7fAtUdfm +agIxvRM4srboP7NK1TbmVufTTkOhGUw0gdW7Rom+VB6m/q3jcHdejQe4a4I9HyxP +rtCiBksZ9+J0X+7PZcbuR5AYtNldDg799p9UTjBttvLmKKhyIVbbwXoGXsyp+Bzm +W/1XWpMRT019jLb87adGR+HPPjAWF+kSe9TJA2XICekHvIxCfsZbi0TTq+5t31gH +Jv71Dn52NCTa6JMLtmFHH3h5JrujN9vvhr2lvQChkK1bb8xzAG4yzjAiOr09gRC3 +hFK8in/6ePLnoEdfnTezzg5mK4NJnM65aY9IDCPdLzN+M0DgvZdM3NEzdOnvA8/w +clnWT1N3r09aNPac5VrnlUC9eNS8pzgQwpshkae3vecT+HLe79uoI0KyYD5XSy5j +HbQ6Fvl9AvMcWuZT7zx/V990Q/iYTRhuOTvyJhIe3iWikcO3zVzUl1UE6afmYmev +Iz8WQpNoPdI82aVk+t1DqYQWtY1a/gAYGT1TfjCnQebWeqAYFhCPklv2pttmAVYj +ThHiYLNR80v3KrrH+fFWynnVAmot23RuBIsM8rhTUIOd2Bxyq4YrE0SGTDKvFJ7H +mn5RRrIbICTsqpx/dLNL1mgiazHdZAFSsnZApXGYiixsgc4QQfApsLlGg3LGZD4a +STJW1+Ds9CQTzKZzMWnYDGCFhLjobH1LfnfzhvOpKA399uuv0L1P3ixgfUt30W+p +d7AkYHE494EPx4ogoj40JBztGS2UvRb0l4tJlPv50v9x+kPXK+50dEmsW7NrCATt +m1NbIGw2QzL9++SFbocf5P7RBdQBTPpXHQeClFduny6IXjQhWk+NKGIXdsVhMcLi +gMjL83LYeY4VPpurFIU8nGVfa7bdTMlh/W5KHpQX4ecibIjxUza0ddbTWxdMTwUk +XgpzvjIrz1XBy79J6H1AuSmh/+e+azmFv1fzkI+KCHGgtp8Q7uY/lDBeuc4zvARF +YMnOqFyA93sMEL28GqjOJLDfERMLuscjIeibbUNDPjwkVErtnQDu9QqLppfnKtJv +dOn20wNndpeo348k2RBO2yVzDTeuCRWkUnTmuhqYpCRopLVfqdv2q6+xM+paW7ZZ +4/+i1Zc= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/pems/rsa_private_unencrypted.pem b/tests/pems/rsa_private_unencrypted.pem new file mode 100644 index 000000000..82406424d --- /dev/null +++ b/tests/pems/rsa_private_unencrypted.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDCFfquKiIv9xfl +izfYicVTnYfy0vXhB5fS1pxl1v4DBwfqciH3uHdFIe8YPtfLq5oxGYaV0CdJVjAk +eqgnmPbKdXNT9EWmv7qfw+zRAWDkaaUSplugWMttLRuxFBxdeeok2xUsQ9PdQu+7 +WsVFtbMAwWM2002Tlb+7QYTNefcPTeMh2EPwgoB2tNF/RUwP1NEJct0yYRTa+R/U +1VSc5Nv91FflADxO0HKVWei5mSdtq9DQn+k+Uct/FSCwrOh3AOUmZs8b0FbBepAK +Y/QOA4Y0hnVfbzUw4FoZuQ1US6e02MJJpDFCN8AtVVTk0B9qXZzAwg22pqhztZr5 +Na+kUQhbAgMBAAECggEAFZPH+NDqWBbKa1Sc8s/uRit/T7mwaEIl2OTPImtSdhe0 +A5aIvDef2um44SMrbpM3YzoJQmKP25FfbM7OHwjcdwmztqOzkqRCJTzs+ReEJCCy +n24rRZpZk1uudnNb6/B/3XUV14P66+BjMpsWz3cx3WWimBfJyhyd4j2YfBeRJfw7 +GLCJ0Jeplj0hKEC4Yo6Dvppnl0DJn8NnsnXLRTwepjwB/EpSxnrpzwBBwzsMTcx/ +2zKC9sZhTE1RDsgbw2IIUiBk1enAhZtmiS/BFT9Y4jWaeXTkkVSnFXPZLYPkdB9W +sHgKGiWOSX/1j90IHaKsSKRFUdn3FHtDTde7o4kGQQKBgQDtdS+WBHfVvBH9iQgw +GWc3KKJPcKHC1m4+GOHhIElb0f5l/y6OTZkvK/bPtKJ8bpufsr9jBVQYubIVfJ2j +ZmO0ukclkzIjzwvY9sSHbnWzFfKbjqNG1zGaZYNe0WM/Lx51pG69hxlVzivLAObf +fqYR0+dt5imD/46FcfHFkTQdCwKBgQDRPchsq4zxxvqMYGxzMfyp7l8y1lLcORHS +j2qkOB0n973DggW2sLIEl3uqf/schpbYO8zFs/1YKrJ5LNnYF14GduugmS5znpnb +YvMJyTXFAqmcbl48ahVUvyOrgxTAOJOfFLRXwZiIVzaAaOop+Ph6A4hEYvXWJ8FW +j6lVr7mz8QKBgFYabArly95At/VLPyDR1U92+IP9v2o6/vadZyqO3org9nJdua/4 +C1fDhVeDlHeyU9PwqN1rDTd5/k00RqT9d6IM+cdyPHgnl5AwysqhDyTFDJfDfQku +9tmZfa1gF7DNkSnvWgh3eIRYoiCWTyEzd1x3ji+Xie5HOJLC4nxVTqRJAoGAZQb6 +rZWLAPX85ShtVJVvFDFW37nh2hjoBQ1gBRhe43xXsH0n+xSHb3YgrKsMeLJ3RMJi +1ZZZHWfIMn+4UwC9Uku66xjq98I9MVMuW6w9/PiTIkeb0nm6AOgk9dvdeg4XILkj +djewSSwq0YdWgJuIhYkNE0/guN0LGZtVvFyTQlECfy3l4m4VwlaPRSSYUxUzULs7 +xc34lL1mf09pWxWebZw4ILQQ90DGWOSD/Zgq6CryRfYgsYqXmGNgDbUFRTWh2DMq +6IoLG3wiqrKSW2oFQL3UOzws0ag7C+6aqKnydpQoEtaP5X+DfAWdAOqnsOP1Ry+W +VTrmtVm4yLiMPBnsw3I= +-----END PRIVATE KEY-----