diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 4f564d79b24f1..a1636a2683b04 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -876,6 +876,10 @@ using command-line tools from OpenSSH_7.6p1 package. * ``ed25519-psw.key``, ``ed25519-psw.key.pub`` - Password-protected Ed25519 private key and corresponding public key. Password is "password". +* ``ed448-nopsw.key.pub`` - + Ed448 private key and corresponding public key. Generated with + ``puttygen`` which added support for Ed448 keys in `PUTTY Ed448`_, + although OpenSSH has not yet added support for them. * ``rsa-nopsw.key``, ``rsa-nopsw.key.pub``, ``rsa-nopsw.key-cert.pub`` - RSA-2048 private key; and corresponding public key in plain format @@ -1097,3 +1101,4 @@ header format (substituting the correct information): .. _`OpenSSL's OCB vectors`: https://github.com/openssl/openssl/commit/2f19ab18a29cf9c82cdd68bc8c7e5be5061b19be .. _`badkeys`: https://github.com/vcsjones/badkeys/tree/50f1cc5f8d13bf3a2046d689f6452decb15d9c3c .. _`OpenSSL's RFC 6979 test vectors`: https://github.com/openssl/openssl/blob/01690a7ff36c4d18c48b301cdf375c954105a1d9/test/recipes/30-test_evp_data/evppkey_ecdsa_rfc6979.txt +.. _`PuTTY Ed448`: https://github.com/github/putty/commit/a085acbadf829ac5b426323ca98058d6aa4048ba diff --git a/src/cryptography/hazmat/primitives/serialization/ssh.py b/src/cryptography/hazmat/primitives/serialization/ssh.py index c01afb0ccdc95..997ba56c022b5 100644 --- a/src/cryptography/hazmat/primitives/serialization/ssh.py +++ b/src/cryptography/hazmat/primitives/serialization/ssh.py @@ -19,6 +19,7 @@ from cryptography.hazmat.primitives.asymmetric import ( dsa, ec, + ed448, ed25519, 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.Ed448PrivateKey, ed448.Ed448PublicKey) + ): + key_type = _SSH_ED448 else: raise ValueError("Unsupported key type") @@ -582,6 +588,57 @@ 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 +693,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_serialization.py b/tests/hazmat/primitives/test_serialization.py index 51fcc3563d8a8..50924825a2280 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1406,11 +1406,6 @@ def test_openssl_serialization_unsupported(self, backend): def test_openssh_serialization_unsupported(self, backend): key = ed448.Ed448PrivateKey.generate() - with pytest.raises(ValueError): - key.public_key().public_bytes( - Encoding.OpenSSH, - PublicFormat.OpenSSH, - ) with pytest.raises(ValueError): key.private_bytes( Encoding.PEM, diff --git a/tests/hazmat/primitives/test_ssh.py b/tests/hazmat/primitives/test_ssh.py index 82f398305e21b..51d4eb4e93dbd 100644 --- a/tests/hazmat/primitives/test_ssh.py +++ b/tests/hazmat/primitives/test_ssh.py @@ -14,6 +14,7 @@ from cryptography.hazmat.primitives.asymmetric import ( dsa, ec, + ed448, ed25519, rsa, ) @@ -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,65 @@ 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): + pub_data = load_vectors_from_file( + os.path.join("asymmetric", "OpenSSH", "ed448-nopsw.key.pub"), + lambda f: f.read(), + mode="rb", + ) + key = load_ssh_public_key(pub_data, backend) + assert isinstance(key, ed448.Ed448PublicKey) + assert ( + key.public_bytes(Encoding.OpenSSH, PublicFormat.OpenSSH) == pub_data + ) + + def test_load_ssh_private_key_unsupported(self, backend): + priv_data = bytearray( + load_vectors_from_file( + os.path.join( + "asymmetric", "OpenSSH", "ed448-nopsw.key" + ), + lambda f: f.read(), + mode="rb", + ) + ) + with raises_unsupported_algorithm( + "Loading Ed448 SSH private keys is unsupported" + ): + load_ssh_private_key(priv_data, backend) + + def test_serialize_ssh_private_unsupported(self, backend): + private_key = ed448.Ed448PrivateKey.generate() + with raises_unsupported_algorithm( + "Serializing Ed448 SSH private keys is unsupported" + ): + private_key.private_bytes( + Encoding.PEM, PrivateFormat.OpenSSH, NoEncryption() + ) + + class TestSSHCertificate: @pytest.mark.supported( only_if=lambda backend: backend.ed25519_supported(), diff --git a/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key b/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key new file mode 100644 index 0000000000000..3162e424c4870 --- /dev/null +++ b/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz +c2gtZWQ0NDgAAAA5WfsOenYSJmThRkWIk/756lJEUf80I1OI4Fn/aivtVCTaJQGB +G2X8qpzJ5tcQWQ8MhrmUluuyXcOAAAAA4JbJTBqWyUwaAAAACXNzaC1lZDQ0OAAA +ADlZ+w56dhImZOFGRYiT/vnqUkRR/zQjU4jgWf9qK+1UJNolAYEbZfyqnMnm1xBZ +DwyGuZSW67Jdw4AAAAByHK1uGuiS00cBcquWyWTyqAHJb5KIA4iF7TSVwBmnI6yr +HHSdOh2EnHF4TajD3t4xTp/QBs9OlMoAWfsOenYSJmThRkWIk/756lJEUf80I1OI +4Fn/aivtVCTaJQGBG2X8qpzJ5tcQWQ8MhrmUluuyXcOAAAAAEmVkZHNhLWtleS0y +MDI0MDcxOAEC +-----END OPENSSH PRIVATE KEY----- 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..994332c9307b0 --- /dev/null +++ b/vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key.pub @@ -0,0 +1 @@ +ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnR1kKYxWp4R72f7vmMVuFImqzJIUKAxJnx23FjBYDQJK2PsoxzyghnPgXNkAYK+UOUIsoPfOrdJwA=