Skip to content

Commit c1274e4

Browse files
committed
Added a convertor for OpenSSH keys.
1 parent ef87bc7 commit c1274e4

File tree

9 files changed

+163
-0
lines changed

9 files changed

+163
-0
lines changed

securesystemslib/convert/ssh.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
This module contains functions to import OpenSSH keys.
3+
"""
4+
5+
import re
6+
import typing
7+
8+
from cryptography.hazmat.primitives.serialization.ssh import (
9+
load_ssh_private_key,
10+
load_ssh_public_key,
11+
)
12+
13+
from .hazmat import import_hazmat_key
14+
15+
openssh_text_format_marker_re = re.compile(
16+
b"^-{2,}BEGIN OPENSSH PRIVATE KEY-{2,}$"
17+
)
18+
19+
20+
def import_ssh_key(
21+
key: typing.Union[str, bytes], password: typing.Optional[bytes] = None
22+
):
23+
"""
24+
<Purpose>
25+
Imports either a public or a private key in OpenSSH format
26+
27+
<Arguments>
28+
key:
29+
A string in OpenSSH format, usually Base64-encoded.
30+
31+
<Exceptions>
32+
securesystemslib.exceptions.FormatError, if the arguments are improperly
33+
formatted.
34+
35+
securesystemslib.exceptions.UnsupportedAlgorithmError, if 'pem' specifies
36+
an unsupported key type.
37+
38+
<Side Effects>
39+
None.
40+
41+
<Returns>
42+
A dictionary containing the keys, conforming to 'securesystemslib.formats.KEY_SCHEMA'.
43+
"""
44+
45+
if isinstance(key, str):
46+
key = key.encode("utf-8")
47+
48+
first_line, _ = key.split(b"\n", 1)
49+
if openssh_text_format_marker_re.match(first_line):
50+
return import_hazmat_key(load_ssh_private_key(key, password))
51+
52+
return import_hazmat_key(load_ssh_public_key(key))

tests/data/ssh/ecdsa

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
3+
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRIz8F5gDezWgzdlRn5Upj7B1NoK43R
4+
wP1d/oiCtNKSYDdql9ds3d+5zzEhnhWvdjGOn1sKwAFlbkz+aueExAkvAAAAqGkmJORpJi
5+
TkAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEjPwXmAN7NaDN2V
6+
GflSmPsHU2grjdHA/V3+iIK00pJgN2qX12zd37nPMSGeFa92MY6fWwrAAWVuTP5q54TECS
7+
8AAAAhAMfqco7t7Cdz3rXsPcUwOINw4CKTqyTpOCFTzjULEB6AAAAACWVjZHNhIGtleQEC
8+
AwQFBg==
9+
-----END OPENSSH PRIVATE KEY-----

tests/data/ssh/ecdsa.pub

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEjPwXmAN7NaDN2VGflSmPsHU2grjdHA/V3+iIK00pJgN2qX12zd37nPMSGeFa92MY6fWwrAAWVuTP5q54TECS8= ecdsa key

tests/data/ssh/ed25519

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
3+
QyNTUxOQAAACAUMmZQY7F/6Pvhj+LQn6HLke1TI0L1qvVImeBCxKjFuAAAAJAh8wZwIfMG
4+
cAAAAAtzc2gtZWQyNTUxOQAAACAUMmZQY7F/6Pvhj+LQn6HLke1TI0L1qvVImeBCxKjFuA
5+
AAAED6uVRyKJZMvslMIFpm4nbqMcRlzza7VwJobiRcbLNwSBQyZlBjsX/o++GP4tCfocuR
6+
7VMjQvWq9UiZ4ELEqMW4AAAAC2VkMjU1MTkga2V5AQI=
7+
-----END OPENSSH PRIVATE KEY-----

tests/data/ssh/ed25519.pub

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBQyZlBjsX/o++GP4tCfocuR7VMjQvWq9UiZ4ELEqMW4 ed25519 key

tests/data/ssh/generate.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env sh
2+
3+
types="ecdsa ed25519 rsa" # dsa
4+
5+
for t in $types; do
6+
yes | ssh-keygen -t $t -C "$t key" -N "" -f ./$t;
7+
done

tests/data/ssh/rsa

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
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
37+
fyozEzAWaCa90AAAAHcnNhIGtleQECAwQ=
38+
-----END OPENSSH PRIVATE KEY-----

tests/data/ssh/rsa.pub

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCzzjkzJGZjENWuFFpjhhK7y+YpMMST18lCK28dlG3QbqbpVJggC+3DcpD4GfFVHTEUpJWplatF4nlpHrzQTc/8RhJzVg+FY3hZXRdxX5J5zJ5vW1aTQdduj0AGMQ0RRaFQo3o5fGMQdrthDUW1/9ybGF0sv4DlTH0rtR8yCvXpl0LmaCIn0MireG8SqsaxdYDb8KdbOHL8Ezo2UmqZg0+svwkphEFoFzqQYVNAhgG4+/5Yotl2PBxZrSUWEUApqvWSS5BeyT/cTnf/8XDGV64H2Qmw9nZhRTIan6GgTgzPOhaX3yUxXS4ZI5rnWRQJuOk/J6FVKsJFNVA+/BAsxjcbUpms+PUtrXe+mHMLcb9FvQAiR9oteNMWUM3X1H8hVRZ20ETTzZVXM44mbSJ+ZvBEcpsGo9RmaMnvsc+4QI9ak/wEojPGN1msNXh/R8RWnhG+sCkbM9Btf2wf1RUyxtpkpYksbcDVyQEl+Rr6x2M+sJH9KZCzS9OFL8hZHKsCAks= rsa key

tests/test_openssh.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
<Program Name>
5+
test_openssh.py
6+
7+
<Author>
8+
KOLANICH
9+
10+
<Started>
11+
Nov 15, 2017
12+
13+
<Copyright>
14+
See LICENSE for licensing information.
15+
16+
<Purpose>
17+
Test OpenSSH-related functions.
18+
19+
"""
20+
21+
import unittest
22+
from pathlib import Path
23+
24+
this_dir = Path(__file__).resolve().absolute().parent
25+
keys_dir = this_dir / "data" / "ssh"
26+
27+
# pylint: disable=wrong-import-position
28+
from securesystemslib.convert.ssh import import_ssh_key
29+
from securesystemslib.keys import create_signature, verify_signature
30+
31+
TEST_DATA = b"test"
32+
33+
34+
class TestOpenSSH(unittest.TestCase):
35+
def test_openssh_import_and_sign_and_verify(self):
36+
files = sorted(set(keys_dir.glob("*.pub")))
37+
for pub_f in files:
38+
sec_f = pub_f.parent / pub_f.stem
39+
with self.subTest(pub_f=pub_f, sec_f=sec_f):
40+
pub = import_ssh_key(pub_f.read_text(), None)
41+
sec = import_ssh_key(sec_f.read_text(), None)
42+
signature = create_signature(sec, TEST_DATA)
43+
self.assertTrue(verify_signature(pub, signature, TEST_DATA))
44+
45+
46+
if __name__ == "__main__":
47+
unittest.main()

0 commit comments

Comments
 (0)