From f2508fe119ba05fb30b9c9d11b3c46df7fb712c6 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 16 Oct 2023 10:44:07 +0200 Subject: [PATCH] add migrate_key cli script This is in preparation for the removal of legacy key modules and formats, in favor of the new Signer API. It allows users to convert their old rsa, ed25519 and ecdsa key files, generated with the `interface` or `keys` module, and using an outdated standard or sslib proprietary format (see #309), to a consistent new standard format, which can be used with the file-based signer (`CryptoSigner`) of the new Signer API. NOTE: The script uses legacy code and should thus be removed with them, from the repo tree, while remaining available to users of securesystemslib for some time. We could keep pointing to it in docs after its removal (users would need to check-out the repo at a specified tag), or move it to a different place. *Change details* * Add cli script to convert key files. * Add private/private encrypted/public test key files for each supported algorithm in the legacy format. **The key pairs were generated with `interface`, minimally modified to allow writing an encrypted and non-encrypted version of the same private key. * Add comprehensive tests, includes backwards/forward compatibility of signatures. Signed-off-by: Lukas Puehringer --- securesystemslib/migrate_key.py | 127 +++++++++++++++++ tests/data/legacy/ecdsa_private_encrypted | 1 + tests/data/legacy/ecdsa_private_unencrypted | 1 + tests/data/legacy/ecdsa_public | 1 + tests/data/legacy/ed25519_private_encrypted | 1 + tests/data/legacy/ed25519_private_unencrypted | 1 + tests/data/legacy/ed25519_public | 1 + tests/data/legacy/rsa_private_encrypted | 42 ++++++ tests/data/legacy/rsa_private_unencrypted | 39 ++++++ tests/data/legacy/rsa_public | 11 ++ tests/test_migrate_key.py | 128 ++++++++++++++++++ 11 files changed, 353 insertions(+) create mode 100755 securesystemslib/migrate_key.py create mode 100644 tests/data/legacy/ecdsa_private_encrypted create mode 100644 tests/data/legacy/ecdsa_private_unencrypted create mode 100755 tests/data/legacy/ecdsa_public create mode 100644 tests/data/legacy/ed25519_private_encrypted create mode 100644 tests/data/legacy/ed25519_private_unencrypted create mode 100755 tests/data/legacy/ed25519_public create mode 100644 tests/data/legacy/rsa_private_encrypted create mode 100644 tests/data/legacy/rsa_private_unencrypted create mode 100755 tests/data/legacy/rsa_public create mode 100644 tests/test_migrate_key.py diff --git a/securesystemslib/migrate_key.py b/securesystemslib/migrate_key.py new file mode 100755 index 00000000..f574a254 --- /dev/null +++ b/securesystemslib/migrate_key.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +"""CLI script to migrate legacy keys to standard format + +Convert legacy key files created via `securesystemslib.interface` or +`securesystemslib.keys` to a standard format, e.g. for use with `CryptoSigner` +of the Signer API (see CRYPTO_SIGNER.md). + +Standard format for all algorithms +---------------------------------- +* private: PEM/PKCS8 +* public: PEM/subjectPublicKeyInfo + +NOTE: Auto-generated keyids are likely to change after migration. Make sure to +set keyids of new signers explicitly, by passing a public key with the desired +keyid, or adopt changes in any delegations in TUF or in-toto. + +""" +import argparse + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey +from cryptography.hazmat.primitives.serialization import ( + BestAvailableEncryption, + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, + load_pem_public_key, +) + +from securesystemslib import interface as legacy +from securesystemslib.signer import CryptoSigner + + +def migrate_private(path_in, algo, password): + """Migrate private key""" + legacy_key = legacy.import_privatekey_from_file(path_in, algo, password) + crypto_signer = CryptoSigner.from_securesystemslib_key(legacy_key) + + if password: + encryption_algorithm = BestAvailableEncryption(password.encode()) + else: + encryption_algorithm = NoEncryption() + + private_key = crypto_signer._private_key # pylint: disable=protected-access + + return private_key.private_bytes( + encoding=Encoding.PEM, + format=PrivateFormat.PKCS8, + encryption_algorithm=encryption_algorithm, + ) + + +def migrate_public(path_in, algo): + """Migrate public key""" + legacy_keys = legacy.import_publickeys_from_file([path_in], [algo]) + legacy_key = list(legacy_keys.values())[0] + + if algo in ["rsa", "ecdsa"]: + public_key = load_pem_public_key( + legacy_key["keyval"]["public"].encode() + ) + else: # ed25519 + public_bytes = bytes.fromhex(legacy_key["keyval"]["public"]) + public_key = Ed25519PublicKey.from_public_bytes(public_bytes) + + return public_key.public_bytes( + encoding=Encoding.PEM, + format=PublicFormat.SubjectPublicKeyInfo, + ) + + +def main(): + parser = argparse.ArgumentParser( + description=( + "Migrate legacy keys to standard format " + "(PEM/PKCS8/subjectPublicKeyInfo)." + ) + ) + + parser.add_argument( + "--type", + choices=["private", "public"], + required=True, + help="key type", + ) + parser.add_argument( + "--password", + help="password to decrypt legacy and encrypt new private key", + ) + parser.add_argument( + "--algo", + choices=["rsa", "ecdsa", "ed25519"], + required=True, + help="key algorithm", + ) + parser.add_argument( + "--in", + dest="path_in", + metavar="PATH", + required=True, + help="file path to legacy key", + ) + parser.add_argument( + "--out", + dest="path_out", + metavar="PATH", + required=True, + help="file path to new key", + ) + + args = parser.parse_args() + + if args.type == "private": + new_key_bytes = migrate_private(args.path_in, args.algo, args.password) + + else: # public + if args.password: + parser.print_usage() + parser.error("use password with --type private only") + new_key_bytes = migrate_public(args.path_in, args.algo) + + with open(args.path_out, "wb+") as output_file: + output_file.write(new_key_bytes) + + +if __name__ == "__main__": + main() diff --git a/tests/data/legacy/ecdsa_private_encrypted b/tests/data/legacy/ecdsa_private_encrypted new file mode 100644 index 00000000..dd291d36 --- /dev/null +++ b/tests/data/legacy/ecdsa_private_encrypted @@ -0,0 +1 @@ +8fbf611c59332aebc82bdcf026fe4119@@@@100000@@@@307a043b7e82a3bf4421f6971e3c5af13570d6824e52e096ab653b877fc52e9f@@@@835eccefffb6b5492c53027a5a92e445@@@@c6513cceb8cb00fd117787a4a8e0331b55b24f7467e7ed881910954deecd23611fd09238a425052ee0fe5647cedaf42ee31e04a9233d368616f8f0668ebd2cc63bf8aa466f438d2aa671ebab9dc4fdacdd5600672480161f43318dd76410145fea99eaa4eb4ef36a4d903007461ac1f123beb72fbb2793ea1d0f7bf9199894d8af43dc880968e882ff5cc342bdebfb827e4158e74aa93b70164324c510aef94389ee4bac90c39277da0ff6a56d41d28e2f019ac90d7008a58e792064ad8e8800925a10bdf161159cc8169bc6bde07bce5b57ffbdaae3f90c825b2f4af9f8a58617f4fcd020922d5b1f6d8b6fe273e59627698acc80ccac5574a5254b1d4e2faae67daeee9dc36c673724d1663ada890a9176d2754eb75c6245f429df773b19b80ef0e18c076f159572acff26a51b15f302473638e08230d137246ab6cada40b26717dda3bd295d9df675f5d025230f9d67d79e70f94cd766abae8afff13f11bb2137e28bb0ed2b4e3675b6a3df707fd89a780ebf6c3c93834eecf7d2ff80240abf7d04957feb613554afb1fd5517320b422ef228608b87740ac2801bd4f9907ac018a9aeb51f36317736c54cc593712219dcf00af3a009ac5a0c762c69cc4d579c425f642f0053668bec77d65bb3f046ed090ebdc502c09fe83e68cc94355be96ec22bd3019043dc6a9e0a3acbb245e710caebc5df7b9ed1c81ec983b6b828f84f8487ac1b068ee8c2cbd91c411041a7a48a4fe59d10e55a7ee0bb276ddde9a3bbfe21bedfd50c094dc161034e3100dfa0309cca8bbe8178a8d5504741e1953075cc25922bca7f19284c51e85669be52687c9c005892c3331057964733b4d5c5b85bfb7bc31bc4a75fcd1f9cd121b19dd48cf5dc \ No newline at end of file diff --git a/tests/data/legacy/ecdsa_private_unencrypted b/tests/data/legacy/ecdsa_private_unencrypted new file mode 100644 index 00000000..5e5cd86a --- /dev/null +++ b/tests/data/legacy/ecdsa_private_unencrypted @@ -0,0 +1 @@ +{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid": "57b7afab61dfd16b96619bb8af6c55483eeade3aa68cf20ff8f0aa69a8bcc8d8", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx+6/aDen+X60RXLETPYz/H4U4qAY\neD/faCdpHBBmyip7xRiyWIrWljDmqcwLfv5wswrqdLF8M6hAdgYjIQZU/A==\n-----END PUBLIC KEY-----", "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIEm0tgzxA8OHiudMGqscqR4QpaJfxwwREqAD3rlSfXGJoAoGCCqGSM49\nAwEHoUQDQgAEx+6/aDen+X60RXLETPYz/H4U4qAYeD/faCdpHBBmyip7xRiyWIrW\nljDmqcwLfv5wswrqdLF8M6hAdgYjIQZU/A==\n-----END EC PRIVATE KEY-----"}, "keyid_hash_algorithms": ["sha256", "sha512"]} diff --git a/tests/data/legacy/ecdsa_public b/tests/data/legacy/ecdsa_public new file mode 100755 index 00000000..cf04be22 --- /dev/null +++ b/tests/data/legacy/ecdsa_public @@ -0,0 +1 @@ +{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx+6/aDen+X60RXLETPYz/H4U4qAY\neD/faCdpHBBmyip7xRiyWIrWljDmqcwLfv5wswrqdLF8M6hAdgYjIQZU/A==\n-----END PUBLIC KEY-----"}} diff --git a/tests/data/legacy/ed25519_private_encrypted b/tests/data/legacy/ed25519_private_encrypted new file mode 100644 index 00000000..b1952ce9 --- /dev/null +++ b/tests/data/legacy/ed25519_private_encrypted @@ -0,0 +1 @@ +9ad267c7c10c74fb754b2d0811cadb6a@@@@100000@@@@06e3a7f38f26fc7c08d28e112fb6b53ab56b84c6214ac040bfcfdafb2fd7d221@@@@52d11d61e2e4ce57f39b3db799e72d24@@@@a5dc0ef8f4c7b1a87ab91c71eac39453004b36fea1284526bc109ae97f179bed1efd5d2a39f69c4a9f246b5ca6cc2e53366f830c11e5d2032c792dfd7f64bc87562b791ca1c1af1cedd245d438033d81688414c1ceaf0883f5cd604311978a60a5239b96fe2ff64ca1cc96becb8aefafe8600dda3b268249bc265cdbd086edbaaf53fd016109125e1dec784390cb8eb1501ccfcdbc4a4608ed8ffbbffd56ec7c97badea762b4a37f00dc7998da8c22c13b0125ffa91465262004a6ed1747a22074a3b32aac64e773f104538fa5032b7667a76f6963bd86d1ec7d3e83e2d59a070d3f558e27e6c8228080f0cc81d07dff2907ef617571ba1b18836743327579285844b1cc6e8ce809a092af8f6e1f7c8d27621348cc35698dec7e1dd82d9d5f07e89bc6d3e192fb1f7822d057265a30f36f18fe376c3c729335605e7d0e0fdbc3f30352f3a2ed94cba2f01f4df015957d \ No newline at end of file diff --git a/tests/data/legacy/ed25519_private_unencrypted b/tests/data/legacy/ed25519_private_unencrypted new file mode 100644 index 00000000..882e543c --- /dev/null +++ b/tests/data/legacy/ed25519_private_unencrypted @@ -0,0 +1 @@ +{"keytype": "ed25519", "scheme": "ed25519", "keyid": "cb2eea1134dac06c1ca2e94b1ffbd15c0bf9f0f541458f0a1df6968a900392f9", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "167ced64cc9908b0bebb92df124d8d7fbe4298d41407524e8d238d0bcdd76c79", "private": "71fe1138357bf15b08723fd01af86deb5b58e4f469eb0acc9892e3c4cf9f4504"}} diff --git a/tests/data/legacy/ed25519_public b/tests/data/legacy/ed25519_public new file mode 100755 index 00000000..a469b32e --- /dev/null +++ b/tests/data/legacy/ed25519_public @@ -0,0 +1 @@ +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "167ced64cc9908b0bebb92df124d8d7fbe4298d41407524e8d238d0bcdd76c79"}} diff --git a/tests/data/legacy/rsa_private_encrypted b/tests/data/legacy/rsa_private_encrypted new file mode 100644 index 00000000..c3aff72f --- /dev/null +++ b/tests/data/legacy/rsa_private_encrypted @@ -0,0 +1,42 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,5196BEDDC2BA3ECB973C1B15CF8BA8F0 + +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 +-----END RSA PRIVATE KEY----- diff --git a/tests/data/legacy/rsa_private_unencrypted b/tests/data/legacy/rsa_private_unencrypted new file mode 100644 index 00000000..ff7a1cc0 --- /dev/null +++ b/tests/data/legacy/rsa_private_unencrypted @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/tests/data/legacy/rsa_public b/tests/data/legacy/rsa_public new file mode 100755 index 00000000..d042405c --- /dev/null +++ b/tests/data/legacy/rsa_public @@ -0,0 +1,11 @@ +-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwYSU3rVOgJV1uXPdkBK+ +Wkx09xMMEQE/xTQG0wX4tLOSeHSK+/MwXeYw83DGYAsHEtEb57j/iNrVSESMEJfB +9CT6YBLh6DfnZQnG5GS5f997t8c3zyGM1fYzoBWaVakMVO2DoM8vxst/c2MU1BJX +aG7hW+Tu5Kuz856YGmX8fV6eGvetWaQM9iN6jRmSS6Wmt/2WXcnvzjV5OyetpkH2 +s8kT60x1zElKiZtmGvz2lnxBZWK3uMBiBL8I52KZF+s9G65+shPJScYFKJ/NLVcM +EaVDCghPay5bZ9pv6vqhylisLIs+alJoxWriz8a7Pc1tW0pBsWrk53adtBaxscFe +3kjiJblQWE19xDwjEVCGA1MmslY0LFi2+R6SVcDDnNK7m0wcaqy1OG1bQXSpZLn/ +bNKE0NvT1SzrlRUojkcneiH6sJSWucXmoo5tL2+uzjEFM/xppW/leo72Umtgahlq +jhvFboJB0slG7ppwcrkuWYg1SejTHBt51jdvzsq6tVtdAgMBAAE= +-----END PUBLIC KEY----- diff --git a/tests/test_migrate_key.py b/tests/test_migrate_key.py new file mode 100644 index 00000000..90e9206b --- /dev/null +++ b/tests/test_migrate_key.py @@ -0,0 +1,128 @@ +"""Test key migration script""" + +import shutil +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from securesystemslib.exceptions import UnverifiedSignatureError +from securesystemslib.interface import ( + import_privatekey_from_file, + import_publickeys_from_file, +) +from securesystemslib.migrate_key import main as migrate_key_cli +from securesystemslib.signer import CryptoSigner, SSlibKey, SSlibSigner + + +class TestMigrateKey(unittest.TestCase): + """Test key migration and backwards compatibility of signatures.""" + + @classmethod + def setUpClass(cls): + cls.old_keys = Path(__file__).parent / "data" / "legacy" + cls.new_keys = Path(tempfile.mkdtemp()) + + # Migrate private, private encrypted and public keys for each algo + for algo in ["rsa", "ecdsa", "ed25519"]: + for type_, name_suffix, has_password in [ + ("private", "_encrypted", True), + ("private", "_unencrypted", False), + ("public", "", False), + ]: + args = [ + "migrate_key.py", + "--type", + type_, + "--algo", + algo, + "--in", + str(cls.old_keys / f"{algo}_{type_}{name_suffix}"), + "--out", + str(cls.new_keys / f"{algo}_{type_}{name_suffix}"), + ] + + if has_password: + args += ["--password", "password"] + + with patch.object(sys, "argv", args): + migrate_key_cli() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.new_keys) + + def test_migrated_keys(self): + for algo in ["rsa", "ecdsa", "ed25519"]: + # Load public key + with open(self.new_keys / f"{algo}_public", "rb") as f: + public_key = SSlibKey.from_pem(f.read()) + + # Load unencrypted private key + path = self.new_keys / f"{algo}_private_unencrypted" + uri = f"file:{path}?encrypted=false" + signer_unenc = CryptoSigner.from_priv_key_uri(uri, public_key) + + # Load encrypted private key + path = self.new_keys / f"{algo}_private_encrypted" + uri = f"file:{path}?encrypted=true" + signer_enc = CryptoSigner.from_priv_key_uri( + uri, public_key, lambda sec: "password" + ) + + # Sign and test signatures + for signer in [signer_unenc, signer_enc]: + sig = signer.sign(b"data") + self.assertIsNone(public_key.verify_signature(sig, b"data")) + with self.assertRaises(UnverifiedSignatureError): + public_key.verify_signature(sig, b"not data") + + def test_new_signature_verifies_with_old_key(self): + for algo in ["rsa", "ecdsa", "ed25519"]: + # Load old public key + key_dicts = import_publickeys_from_file( + [str(self.old_keys / f"{algo}_public")], [algo] + ) + key_dict = list(key_dicts.values())[0] + public_key = SSlibKey.from_securesystemslib_key(key_dict) + + # Load new private key + # NOTE: The signer is loaded with the old public key, thus the old + # keyid will be assigned to any new signatures. + path = self.new_keys / f"{algo}_private_unencrypted" + uri = f"file:{path}?encrypted=false" + signer = CryptoSigner.from_priv_key_uri(uri, public_key) + + # Sign and test signatures + sig = signer.sign(b"data") + self.assertIsNone(public_key.verify_signature(sig, b"data")) + with self.assertRaises(UnverifiedSignatureError): + public_key.verify_signature(sig, b"not data") + + def test_old_signature_verifies_with_new_key(self): + for algo in ["rsa", "ecdsa", "ed25519"]: + # Load old private key + private_key = import_privatekey_from_file( + str(self.old_keys / f"{algo}_private_unencrypted"), algo + ) + signer = SSlibSigner(private_key) + + # Load new public key + with open(self.new_keys / f"{algo}_public", "rb") as f: + # NOTE: The new auto-keyid would differ from the old keyid. + # Set it explicitly, to verify signatures with old keyid below + public_key = SSlibKey.from_pem( + f.read(), keyid=private_key["keyid"] + ) + + # Sign and test signature + sig = signer.sign(b"data") + self.assertIsNone(public_key.verify_signature(sig, b"data")) + with self.assertRaises(UnverifiedSignatureError): + public_key.verify_signature(sig, b"not data") + + +# Run the unit tests. +if __name__ == "__main__": + unittest.main()