diff --git a/src/cryptography/hazmat/primitives/serialization/ssh.py b/src/cryptography/hazmat/primitives/serialization/ssh.py index c01afb0ccdc95..61f5a6652ad4f 100644 --- a/src/cryptography/hazmat/primitives/serialization/ssh.py +++ b/src/cryptography/hazmat/primitives/serialization/ssh.py @@ -20,6 +20,7 @@ dsa, ec, ed25519, + ed448, padding, rsa, ) @@ -57,6 +58,7 @@ def _bcrypt_kdf( _SSH_ED25519 = b"ssh-ed25519" +_SSH_ED448 = b"ssh-ed448" _SSH_RSA = b"ssh-rsa" _SSH_DSA = b"ssh-dss" _ECDSA_NISTP256 = b"ecdsa-sha2-nistp256" @@ -152,6 +154,10 @@ def _get_ssh_key_type(key: SSHPrivateKeyTypes | SSHPublicKeyTypes) -> bytes: key, (ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey) ): key_type = _SSH_ED25519 + elif isinstance( + key, (ed448.Ed448PublicKey,) # private keys are not yet supported + ): + key_type = _SSH_ED448 else: raise ValueError("Unsupported key type") @@ -582,6 +588,59 @@ def encode_private( f_priv.put_sshstr(f_keypair) +class _SSHFormatEd448: + """Format for Ed448 keys. + + Public: + bytes point + Private: + bytes point + bytes secret_and_point + """ + + def get_public( + self, data: memoryview + ) -> tuple[tuple[memoryview], memoryview]: + """Ed448 public fields""" + point, data = _get_sshstr(data) + return (point,), data + + def load_public( + self, data: memoryview + ) -> tuple[ed448.Ed448PublicKey, memoryview]: + """Make Ed448 public key from data.""" + (point,), data = self.get_public(data) + public_key = ed448.Ed448PublicKey.from_public_bytes( + point.tobytes() + ) + return public_key, data + + def load_private( + self, data: memoryview, pubfields + ) -> tuple[ed448.Ed448PrivateKey, memoryview]: + """Make Ed448 private key from data.""" + raise UnsupportedAlgorithm( + "Loading Ed448 SSH private keys is unsupported" + ) + + def encode_public( + self, public_key: ed448.Ed448PublicKey, f_pub: _FragList + ) -> None: + """Write Ed448 public key""" + raw_public_key = public_key.public_bytes( + Encoding.Raw, PublicFormat.Raw + ) + f_pub.put_sshstr(raw_public_key) + + def encode_private( + self, private_key: ed448.Ed448PrivateKey, f_priv: _FragList + ) -> None: + """Write Ed448 private key""" + raise UnsupportedAlgorithm( + "Serializing Ed448 SSH private keys is unsupported" + ) + + def load_application(data) -> tuple[memoryview, memoryview]: """ U2F application strings @@ -636,6 +695,7 @@ def load_public( _SSH_RSA: _SSHFormatRSA(), _SSH_DSA: _SSHFormatDSA(), _SSH_ED25519: _SSHFormatEd25519(), + _SSH_ED448: _SSHFormatEd448(), _ECDSA_NISTP256: _SSHFormatECDSA(b"nistp256", ec.SECP256R1()), _ECDSA_NISTP384: _SSHFormatECDSA(b"nistp384", ec.SECP384R1()), _ECDSA_NISTP521: _SSHFormatECDSA(b"nistp521", ec.SECP521R1()), diff --git a/tests/hazmat/primitives/test_ssh.py b/tests/hazmat/primitives/test_ssh.py index 82f398305e21b..0940eabe846db 100644 --- a/tests/hazmat/primitives/test_ssh.py +++ b/tests/hazmat/primitives/test_ssh.py @@ -15,6 +15,7 @@ dsa, ec, ed25519, + ed448, rsa, ) from cryptography.hazmat.primitives.serialization import ( @@ -55,6 +56,7 @@ class TestOpenSSHSerialization: ("ecdsa-nopsw.key.pub", "ecdsa-nopsw.key-cert.pub"), ("ed25519-psw.key.pub", None), ("ed25519-nopsw.key.pub", "ed25519-nopsw.key-cert.pub"), + ("ed448-nopsw.key.pub", None), ("sk-ecdsa-psw.key.pub", None), ("sk-ecdsa-nopsw.key.pub", None), ("sk-ed25519-psw.key.pub", None), @@ -64,6 +66,8 @@ class TestOpenSSHSerialization: def test_load_ssh_public_key(self, key_file, cert_file, backend): if "ed25519" in key_file and not backend.ed25519_supported(): pytest.skip("Requires OpenSSL with Ed25519 support") + if "ed448" in key_file and not backend.ed448_supported(): + pytest.skip("Requires OpenSSL with Ed448 support") # normal public key pub_data = load_vectors_from_file( @@ -170,6 +174,8 @@ def run_partial_pubkey(self, pubdata, backend): def test_load_ssh_private_key(self, key_file, backend): if "ed25519" in key_file and not backend.ed25519_supported(): pytest.skip("Requires OpenSSL with Ed25519 support") + if "ed448" in key_file and not backend.ed448_supported(): + pytest.skip("Requires OpenSSL with Ed448 support") if "-psw" in key_file and not ssh._bcrypt_supported: pytest.skip("Requires bcrypt module") @@ -1130,6 +1136,40 @@ def test_load_ssh_public_key_trailing_data(self, backend): load_ssh_public_key(ssh_key, backend) +@pytest.mark.supported( + only_if=lambda backend: backend.ed448_supported(), + skip_message="Requires OpenSSL with Ed448 support", +) +class TestEd448SSHSerialization: + def test_load_ssh_public_key(self, backend): + ssh_key = ( + b"ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnVY+2PC4Oj9MSsYZORD7xivKK3zy" + b"yHFKYj3eMCMPsAwNVk6fqGHeSIRDN39ld5Jto8S5Y1lemtJHA= user@chir" + b"on.local" + ) + key = load_ssh_public_key(ssh_key, backend) + assert isinstance(key, ed448.Ed448PublicKey) + assert key.public_bytes(Encoding.Raw, PublicFormat.Raw) == ( + bytes.fromhex( + "d5 63 ed 8f 0b 83 a3 f4 c4 ac 61 93 91 0f bc 62" + "bc a2 b7 cf 2c 87 14 a6 23 dd e3 02 30 fb 00 c0" + "d5 64 e9 fa 86 1d e4 88 44 33 77 f6 57 79 26 da" + "3c 4b 96 35 95 e9 ad 24 70" + ) + ) + + def test_public_bytes_openssh(self, backend): + ssh_key = ( + b"ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnVY+2PC4Oj9MSsYZORD7xivKK3zy" + b"yHFKYj3eMCMPsAwNVk6fqGHeSIRDN39ld5Jto8S5Y1lemtJHA=" + ) + key = load_ssh_public_key(ssh_key, backend) + assert isinstance(key, ed448.Ed448PublicKey) + assert ( + key.public_bytes(Encoding.OpenSSH, PublicFormat.OpenSSH) == ssh_key + ) + + class TestSSHCertificate: @pytest.mark.supported( only_if=lambda backend: backend.ed25519_supported(), diff --git a/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key.pub b/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key.pub new file mode 100644 index 0000000000000..a067101ab16d7 --- /dev/null +++ b/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key.pub @@ -0,0 +1 @@ +ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnVY+2PC4Oj9MSsYZORD7xivKK3zyyHFKYj3eMCMPsAwNVk6fqGHeSIRDN39ld5Jto8S5Y1lemtJHA=