Fix SSH private-key import/export for Paramiko 5.1.0 and remove unconditional DSSKey reference

- Replace paramiko-based OpenSSH private key serialization with
  cryptography's PrivateFormat.OpenSSH backend.
  The old code relied on paramiko.RSAKey, paramiko.DSSKey, etc. to write
  private keys, but Paramiko 5.1.0 removed DSSKey entirely and changed
  constructor APIs, causing crashes.

- Add explicit fallback path in _load_ssh_private_with_paramiko that
  guards against missing DSSKey (getattr paramiko 'DSSKey', append only
  when present), modern private_key attribute vs legacy signing_key,
  and Ed25519 PyNaCl key material conversion.

- Add two new test coverage methods:
  test_ssh_private_roundtrip  – round-trip RSA/DSA/EC/Ed25519
    unencrypted and password-encrypted, verifying OpenSSH PEM header
    and DER preservation.
  test_ssh_private_paramiko_fallback – explicitly exercise the
    Paramiko fallback for RSA and EC keys, confirming it still works
    under locked Paramiko 5.1.0.

Also clean up the comment in load_key_bytes to reflect the new fallback
behavior rather than the old 'encrypted OpenSSH keys not supported' note.
This commit is contained in:
2026-10-26 12:06:16 +08:00
parent bd352051e0
commit 987fdc0e12
2 changed files with 51 additions and 24 deletions
@@ -192,6 +192,35 @@ class KeyTest(unittest.TestCase):
self.assertTrue(private)
self.assertEqual(keys.public_der(loaded), keys.public_der(key))
def test_ssh_private_roundtrip(self) -> None:
for key_type, kwargs in [
("rsa", {"size": 2048}),
("dsa", {"size": 1024}),
("ec", {"curve": "secp256r1"}),
("ed25519", {}),
]:
key = keys.generate_key(key_type, **kwargs)
for password in (None, "secret"):
with self.subTest(key_type=key_type, password=password):
encoded = keys.ssh_private(key, password=password)
self.assertTrue(encoded.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----"))
loaded, private = keys.load_key_bytes(encoded, password=password)
self.assertTrue(private)
self.assertEqual(keys.private_der(loaded), keys.private_der(key))
def test_ssh_private_paramiko_fallback(self) -> None:
for key_type, kwargs in [
("rsa", {"size": 2048}),
("ec", {"curve": "secp256r1"}),
("ed25519", {}),
]:
key = keys.generate_key(key_type, **kwargs)
for password in (None, "secret"):
with self.subTest(key_type=key_type, password=password):
encoded = keys.ssh_private(key, password=password)
loaded = keys._load_ssh_private_with_paramiko(encoded, password=password)
self.assertEqual(keys.private_der(loaded), keys.private_der(key))
class CertificateTest(unittest.TestCase):
def test_issue_and_verify(self) -> None: