Skip to content

Commit eed49e2

Browse files
authored
Merge pull request #323 from pmazzini/ssh
export to ssh
2 parents fafbcaf + 52e2f06 commit eed49e2

File tree

4 files changed

+147
-8
lines changed

4 files changed

+147
-8
lines changed

src/ecdsa/der.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import warnings
66
from itertools import chain
77
from six import int2byte, b, text_type
8-
from ._compat import str_idx_as_int
8+
from ._compat import compat26_str, str_idx_as_int
99

1010

1111
class UnexpectedDER(Exception):
@@ -400,10 +400,10 @@ def unpem(pem):
400400

401401

402402
def topem(der, name):
403-
b64 = base64.b64encode(der)
403+
b64 = base64.b64encode(compat26_str(der))
404404
lines = [("-----BEGIN %s-----\n" % name).encode()]
405405
lines.extend(
406-
[b64[start : start + 64] + b("\n") for start in range(0, len(b64), 64)]
406+
[b64[start : start + 76] + b("\n") for start in range(0, len(b64), 76)]
407407
)
408408
lines.append(("-----END %s-----\n" % name).encode())
409409
return b("").join(lines)

src/ecdsa/keys.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
from six import PY2, b
99
from . import ecdsa, eddsa
10-
from . import der
10+
from . import der, ssh
1111
from . import rfc6979
1212
from . import ellipticcurve
1313
from .curves import NIST192p, Curve, Ed25519, Ed448
@@ -614,6 +614,18 @@ def to_der(
614614
der.encode_bitstring(point_str, 0),
615615
)
616616

617+
def to_ssh(self):
618+
"""
619+
Convert the public key to the SSH format.
620+
621+
:return: SSH encoding of the public key
622+
:rtype: bytes
623+
"""
624+
return ssh.serialize_public(
625+
self.curve.name,
626+
self.to_string(),
627+
)
628+
617629
def verify(
618630
self,
619631
signature,
@@ -1281,6 +1293,19 @@ def to_der(
12811293
der.encode_octet_string(ec_private_key),
12821294
)
12831295

1296+
def to_ssh(self):
1297+
"""
1298+
Convert the private key to the SSH format.
1299+
1300+
:return: SSH encoded private key
1301+
:rtype: bytes
1302+
"""
1303+
return ssh.serialize_private(
1304+
self.curve.name,
1305+
self.verifying_key.to_string(),
1306+
self.to_string(),
1307+
)
1308+
12841309
def get_verifying_key(self):
12851310
"""
12861311
Return the VerifyingKey associated with this private key.

src/ecdsa/ssh.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import binascii
2+
from . import der
3+
from ._compat import compat26_str, int_to_bytes
4+
5+
_SSH_ED25519 = b"ssh-ed25519"
6+
_SK_MAGIC = b"openssh-key-v1\0"
7+
_NONE = b"none"
8+
9+
10+
def _get_key_type(name):
11+
if name == "Ed25519":
12+
return _SSH_ED25519
13+
else:
14+
raise ValueError("Unsupported key type")
15+
16+
17+
class _Serializer:
18+
def __init__(self):
19+
self.bytes = b""
20+
21+
def put_raw(self, val):
22+
self.bytes += val
23+
24+
def put_u32(self, val):
25+
self.bytes += int_to_bytes(val, length=4, byteorder="big")
26+
27+
def put_str(self, val):
28+
self.put_u32(len(val))
29+
self.bytes += val
30+
31+
def put_pad(self, blklen=8):
32+
padlen = blklen - (len(self.bytes) % blklen)
33+
self.put_raw(bytearray(range(1, 1 + padlen)))
34+
35+
def encode(self):
36+
return binascii.b2a_base64(compat26_str(self.bytes))
37+
38+
def tobytes(self):
39+
return self.bytes
40+
41+
def topem(self):
42+
return der.topem(self.bytes, "OPENSSH PRIVATE KEY")
43+
44+
45+
def serialize_public(name, pub):
46+
serial = _Serializer()
47+
ktype = _get_key_type(name)
48+
serial.put_str(ktype)
49+
serial.put_str(pub)
50+
return b" ".join([ktype, serial.encode()])
51+
52+
53+
def serialize_private(name, pub, priv):
54+
# encode public part
55+
spub = _Serializer()
56+
ktype = _get_key_type(name)
57+
spub.put_str(ktype)
58+
spub.put_str(pub)
59+
60+
# encode private part
61+
spriv = _Serializer()
62+
checksum = 0
63+
spriv.put_u32(checksum)
64+
spriv.put_u32(checksum)
65+
spriv.put_raw(spub.tobytes())
66+
spriv.put_str(priv + pub)
67+
comment = b""
68+
spriv.put_str(comment)
69+
spriv.put_pad()
70+
71+
# top-level structure
72+
main = _Serializer()
73+
main.put_raw(_SK_MAGIC)
74+
ciphername = kdfname = _NONE
75+
main.put_str(ciphername)
76+
main.put_str(kdfname)
77+
nokdf = 0
78+
main.put_u32(nokdf)
79+
nkeys = 1
80+
main.put_u32(nkeys)
81+
main.put_str(spub.tobytes())
82+
main.put_str(spriv.tobytes())
83+
return main.topem()

src/ecdsa/test_keys.py

+35-4
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,18 @@ def test_export_ed255_to_pem(self):
364364

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

367+
def test_export_ed255_to_ssh(self):
368+
vk_str = (
369+
b"\x23\x00\x50\xd0\xd6\x64\x22\x28\x8e\xe3\x55\x89\x7e\x6e\x41\x57"
370+
b"\x8d\xae\xde\x44\x26\xee\x56\x27\xbc\x85\xe6\x0b\x2f\x2a\xcb\x65"
371+
)
372+
373+
vk = VerifyingKey.from_string(vk_str, Ed25519)
374+
375+
vk_ssh = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMAUNDWZCIojuNViX5uQVeNrt5EJu5WJ7yF5gsvKstl\n"
376+
377+
self.assertEqual(vk_ssh, vk.to_ssh())
378+
367379
def test_ed25519_export_import(self):
368380
sk = SigningKey.generate(Ed25519)
369381
vk = sk.verifying_key
@@ -428,8 +440,8 @@ def test_ed448_to_pem(self):
428440

429441
vk_pem = (
430442
b"-----BEGIN PUBLIC KEY-----\n"
431-
b"MEMwBQYDK2VxAzoAeQtetSu7CMEzE+XWB10Bg47LCA0giNikOxHzdp+tZ/eK/En0\n"
432-
b"dTdYD2ll94g58MhSnBiBQB9A1MMA\n"
443+
b"MEMwBQYDK2VxAzoAeQtetSu7CMEzE+XWB10Bg47LCA0giNikOxHzdp+tZ/eK/En0dTdYD2ll94g5\n"
444+
b"8MhSnBiBQB9A1MMA\n"
433445
b"-----END PUBLIC KEY-----\n"
434446
)
435447

@@ -629,6 +641,25 @@ def test_ed25519_to_pem(self):
629641

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

644+
def test_ed25519_to_ssh(self):
645+
sk = SigningKey.from_string(
646+
b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C"
647+
b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4",
648+
Ed25519,
649+
)
650+
651+
ssh_str = (
652+
b"-----BEGIN OPENSSH PRIVATE KEY-----\n"
653+
b"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUx\n"
654+
b"OQAAACAjAFDQ1mQiKI7jVYl+bkFXja7eRCbuVie8heYLLyrLZQAAAIgAAAAAAAAAAAAAAAtzc2gt\n"
655+
b"ZWQyNTUxOQAAACAjAFDQ1mQiKI7jVYl+bkFXja7eRCbuVie8heYLLyrLZQAAAEA0usfRTtTxvE+M\n"
656+
b"SD4PGXdM/Li+rFRmRRGa19e4Bwv11CMAUNDWZCIojuNViX5uQVeNrt5EJu5WJ7yF5gsvKstlAAAA\n"
657+
b"AAECAwQF\n"
658+
b"-----END OPENSSH PRIVATE KEY-----\n"
659+
)
660+
661+
self.assertEqual(sk.to_ssh(), ssh_str)
662+
632663
def test_ed25519_to_and_from_pem(self):
633664
sk = SigningKey.generate(Ed25519)
634665

@@ -665,8 +696,8 @@ def test_ed448_to_pem(self):
665696
)
666697
pem_str = (
667698
b"-----BEGIN PRIVATE KEY-----\n"
668-
b"MEcCAQAwBQYDK2VxBDsEOTyFuXqFLXgJlV8uDqcOw9nG4IqzLiZ/i5NfBDoHPzmP\n"
669-
b"OP0JMYaLGlTzwovmvCDJ2zLaezu9NLz9aQ==\n"
699+
b"MEcCAQAwBQYDK2VxBDsEOTyFuXqFLXgJlV8uDqcOw9nG4IqzLiZ/i5NfBDoHPzmPOP0JMYaLGlTz\n"
700+
b"wovmvCDJ2zLaezu9NLz9aQ==\n"
670701
b"-----END PRIVATE KEY-----\n"
671702
)
672703

0 commit comments

Comments
 (0)