Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

export to ssh #323

Merged
merged 1 commit into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/ecdsa/der.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import warnings
from itertools import chain
from six import int2byte, b, text_type
from ._compat import str_idx_as_int
from ._compat import compat26_str, str_idx_as_int


class UnexpectedDER(Exception):
Expand Down Expand Up @@ -400,10 +400,10 @@ def unpem(pem):


def topem(der, name):
b64 = base64.b64encode(der)
b64 = base64.b64encode(compat26_str(der))
lines = [("-----BEGIN %s-----\n" % name).encode()]
lines.extend(
[b64[start : start + 64] + b("\n") for start in range(0, len(b64), 64)]
[b64[start : start + 76] + b("\n") for start in range(0, len(b64), 76)]
)
lines.append(("-----END %s-----\n" % name).encode())
return b("").join(lines)
27 changes: 26 additions & 1 deletion src/ecdsa/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os
from six import PY2, b
from . import ecdsa, eddsa
from . import der
from . import der, ssh
from . import rfc6979
from . import ellipticcurve
from .curves import NIST192p, Curve, Ed25519, Ed448
Expand Down Expand Up @@ -614,6 +614,18 @@ def to_der(
der.encode_bitstring(point_str, 0),
)

def to_ssh(self):
pmazzini marked this conversation as resolved.
Show resolved Hide resolved
"""
Convert the public key to the SSH format.

:return: SSH encoding of the public key
:rtype: bytes
"""
return ssh.serialize_public(
self.curve.name,
self.to_string(),
)

def verify(
self,
signature,
Expand Down Expand Up @@ -1281,6 +1293,19 @@ def to_der(
der.encode_octet_string(ec_private_key),
)

def to_ssh(self):
pmazzini marked this conversation as resolved.
Show resolved Hide resolved
"""
Convert the private key to the SSH format.

:return: SSH encoded private key
:rtype: bytes
"""
return ssh.serialize_private(
self.curve.name,
self.verifying_key.to_string(),
self.to_string(),
)

def get_verifying_key(self):
"""
Return the VerifyingKey associated with this private key.
Expand Down
83 changes: 83 additions & 0 deletions src/ecdsa/ssh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import binascii
from . import der
from ._compat import compat26_str, int_to_bytes

_SSH_ED25519 = b"ssh-ed25519"
_SK_MAGIC = b"openssh-key-v1\0"
_NONE = b"none"


def _get_key_type(name):
if name == "Ed25519":
return _SSH_ED25519
else:
raise ValueError("Unsupported key type")


class _Serializer:
def __init__(self):
self.bytes = b""

def put_raw(self, val):
self.bytes += val

def put_u32(self, val):
self.bytes += int_to_bytes(val, length=4, byteorder="big")

def put_str(self, val):
self.put_u32(len(val))
self.bytes += val

def put_pad(self, blklen=8):
padlen = blklen - (len(self.bytes) % blklen)
self.put_raw(bytearray(range(1, 1 + padlen)))

def encode(self):
return binascii.b2a_base64(compat26_str(self.bytes))

def tobytes(self):
return self.bytes

def topem(self):
return der.topem(self.bytes, "OPENSSH PRIVATE KEY")


def serialize_public(name, pub):
serial = _Serializer()
ktype = _get_key_type(name)
serial.put_str(ktype)
serial.put_str(pub)
return b" ".join([ktype, serial.encode()])


def serialize_private(name, pub, priv):
# encode public part
spub = _Serializer()
ktype = _get_key_type(name)
spub.put_str(ktype)
spub.put_str(pub)

# encode private part
spriv = _Serializer()
checksum = 0
spriv.put_u32(checksum)
spriv.put_u32(checksum)
spriv.put_raw(spub.tobytes())
spriv.put_str(priv + pub)
comment = b""
spriv.put_str(comment)
spriv.put_pad()

# top-level structure
main = _Serializer()
main.put_raw(_SK_MAGIC)
ciphername = kdfname = _NONE
main.put_str(ciphername)
main.put_str(kdfname)
nokdf = 0
main.put_u32(nokdf)
nkeys = 1
main.put_u32(nkeys)
main.put_str(spub.tobytes())
main.put_str(spriv.tobytes())
return main.topem()
pmazzini marked this conversation as resolved.
Show resolved Hide resolved
39 changes: 35 additions & 4 deletions src/ecdsa/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,18 @@ def test_export_ed255_to_pem(self):

self.assertEqual(vk_pem, vk.to_pem())

def test_export_ed255_to_ssh(self):
vk_str = (
b"\x23\x00\x50\xd0\xd6\x64\x22\x28\x8e\xe3\x55\x89\x7e\x6e\x41\x57"
b"\x8d\xae\xde\x44\x26\xee\x56\x27\xbc\x85\xe6\x0b\x2f\x2a\xcb\x65"
)

vk = VerifyingKey.from_string(vk_str, Ed25519)

vk_ssh = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMAUNDWZCIojuNViX5uQVeNrt5EJu5WJ7yF5gsvKstl\n"

self.assertEqual(vk_ssh, vk.to_ssh())

def test_ed25519_export_import(self):
sk = SigningKey.generate(Ed25519)
vk = sk.verifying_key
Expand Down Expand Up @@ -428,8 +440,8 @@ def test_ed448_to_pem(self):

vk_pem = (
b"-----BEGIN PUBLIC KEY-----\n"
b"MEMwBQYDK2VxAzoAeQtetSu7CMEzE+XWB10Bg47LCA0giNikOxHzdp+tZ/eK/En0\n"
b"dTdYD2ll94g58MhSnBiBQB9A1MMA\n"
b"MEMwBQYDK2VxAzoAeQtetSu7CMEzE+XWB10Bg47LCA0giNikOxHzdp+tZ/eK/En0dTdYD2ll94g5\n"
b"8MhSnBiBQB9A1MMA\n"
b"-----END PUBLIC KEY-----\n"
)

Expand Down Expand Up @@ -629,6 +641,25 @@ def test_ed25519_to_pem(self):

self.assertEqual(sk.to_pem(format="pkcs8"), pem_str)

def test_ed25519_to_ssh(self):
sk = SigningKey.from_string(
b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C"
b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4",
Ed25519,
)

ssh_str = (
b"-----BEGIN OPENSSH PRIVATE KEY-----\n"
b"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUx\n"
b"OQAAACAjAFDQ1mQiKI7jVYl+bkFXja7eRCbuVie8heYLLyrLZQAAAIgAAAAAAAAAAAAAAAtzc2gt\n"
b"ZWQyNTUxOQAAACAjAFDQ1mQiKI7jVYl+bkFXja7eRCbuVie8heYLLyrLZQAAAEA0usfRTtTxvE+M\n"
b"SD4PGXdM/Li+rFRmRRGa19e4Bwv11CMAUNDWZCIojuNViX5uQVeNrt5EJu5WJ7yF5gsvKstlAAAA\n"
b"AAECAwQF\n"
b"-----END OPENSSH PRIVATE KEY-----\n"
)

self.assertEqual(sk.to_ssh(), ssh_str)

def test_ed25519_to_and_from_pem(self):
sk = SigningKey.generate(Ed25519)

Expand Down Expand Up @@ -665,8 +696,8 @@ def test_ed448_to_pem(self):
)
pem_str = (
b"-----BEGIN PRIVATE KEY-----\n"
b"MEcCAQAwBQYDK2VxBDsEOTyFuXqFLXgJlV8uDqcOw9nG4IqzLiZ/i5NfBDoHPzmP\n"
b"OP0JMYaLGlTzwovmvCDJ2zLaezu9NLz9aQ==\n"
b"MEcCAQAwBQYDK2VxBDsEOTyFuXqFLXgJlV8uDqcOw9nG4IqzLiZ/i5NfBDoHPzmPOP0JMYaLGlTz\n"
b"wovmvCDJ2zLaezu9NLz9aQ==\n"
b"-----END PRIVATE KEY-----\n"
)

Expand Down
Loading