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 17, 2024
1 parent 3c7a5e0 commit 0ac431c
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 5 deletions.
2 changes: 2 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,8 @@ 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 public key.
* ``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
59 changes: 59 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,11 @@ 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")

Expand Down Expand Up @@ -582,6 +589,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 +694,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
5 changes: 0 additions & 5 deletions tests/hazmat/primitives/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 40 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,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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnVY+2PC4Oj9MSsYZORD7xivKK3zyyHFKYj3eMCMPsAwNVk6fqGHeSIRDN39ld5Jto8S5Y1lemtJHA=

0 comments on commit 0ac431c

Please sign in to comment.