Skip to content

Commit

Permalink
Support loading Ed448 public keys in OpenSSH format
Browse files Browse the repository at this point in the history
The 'ssh-ed448' key type is documented along with 'ssh-ed25519' in [1], but
has never been supported by any as-yet-released version of OpenSSH.

However, LANcom router devices (which appear to be primarily used in
Germany, see [2] for examples on the public Internet) appear to support
these keys, so this library can and should support loading them.

Ed448 private keys are not yet implemented here, because OpenSSH itself does
not yet support them, and it is the de facto authority for private key
formats.  However, PuTTY has already implemented support for generating and
using Ed448 keys, and the PuTTY developers note in [3] that the OpenSSH
developers are in agreement with them as to the correct Ed448 private key
format:

> I checked with them [OpenSSH developers], and they agreed that there's an
> obviously right format for Ed448 keys, which is to do them exactly like
> Ed25519 except that you have a 57-byte string everywhere Ed25519 had a
> 32-byte string.  So I've done that.

See also [4] in which I extended `ssh-audit` to allow it to scan and
discover host keys of type 'ssh-ed488'.

[1] https://datatracker.ietf.org/doc/html/rfc8709#name-public-key-format
[2] https://www.shodan.io/search?query=ssh+%22ed448%22
[3] github/putty@a085acb
[4] jtesta/ssh-audit#277
  • Loading branch information
dlenskiSB committed Jul 18, 2024
1 parent 14772c2 commit f2d8f73
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 8 deletions.
5 changes: 5 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
58 changes: 58 additions & 0 deletions src/cryptography/hazmat/primitives/serialization/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from cryptography.hazmat.primitives.asymmetric import (
dsa,
ec,
ed448,
ed25519,
padding,
rsa,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()),
Expand Down
15 changes: 7 additions & 8 deletions tests/hazmat/primitives/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
)
from cryptography.hazmat.primitives.serialization.pkcs12 import PBES

from ...utils import load_vectors_from_file
from ...utils import load_vectors_from_file, raises_unsupported_algorithm
from .test_ec import _skip_curve_unsupported
from .test_rsa import rsa_key_2048
from .utils import _check_dsa_private_numbers, _check_rsa_private_numbers
Expand Down Expand Up @@ -1391,7 +1391,9 @@ def test_load_public_key(self, key_path, encoding, loader, backend):

def test_openssl_serialization_unsupported(self, backend):
key = ed448.Ed448PrivateKey.generate()
with pytest.raises(ValueError):
with raises_unsupported_algorithm(
"Serializing Ed448 SSH private keys is unsupported"
):
key.private_bytes(
Encoding.PEM,
PrivateFormat.TraditionalOpenSSL,
Expand All @@ -1406,12 +1408,9 @@ 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):
with raises_unsupported_algorithm(
"Loading Ed448 SSH private keys is unsupported"
):
key.private_bytes(
Encoding.PEM,
PrivateFormat.OpenSSH,
Expand Down
65 changes: 65 additions & 0 deletions tests/hazmat/primitives/test_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from cryptography.hazmat.primitives.asymmetric import (
dsa,
ec,
ed448,
ed25519,
rsa,
)
Expand Down Expand Up @@ -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),
Expand All @@ -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(
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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(),
Expand Down
10 changes: 10 additions & 0 deletions vectors/cryptography_vectors/asymmetric/OpenSSH/ed448-nopsw.key
Original file line number Diff line number Diff line change
@@ -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-----
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnR1kKYxWp4R72f7vmMVuFImqzJIUKAxJnx23FjBYDQJK2PsoxzyghnPgXNkAYK+UOUIsoPfOrdJwA=

0 comments on commit f2d8f73

Please sign in to comment.