Skip to content

Commit bafd936

Browse files
authored
Merge pull request #43 from ojarva/parse-authorized-files
Add support for parsing authorized_keys files
2 parents 60c2299 + 1d67246 commit bafd936

7 files changed

Lines changed: 126 additions & 8 deletions

File tree

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[settings]
22
line_length = 120
3-
3+
not_skip = __init__.py

README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ Usage:
5959
print(ssh.options_raw) # None (string of optional options at the beginning of public key)
6060
print(ssh.options) # None (options as a dictionary, parsed and validated)
6161

62+
63+
Parsing of `authorized_keys` files:
64+
65+
::
66+
67+
from sshpubkeys import AuthorizedKeysFile
68+
69+
key_file = AuthorizedKeysFile("""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEGODBKRjsFB/1v3pDRGpA6xR+QpOJg9vat0brlbUNDD\n"""
70+
"""#This is a comment\n\n"""
71+
"""ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF9QpvUneTvt8"""
72+
"""lu0ePSuzr7iLE9ZMPu2DFTmqh7BVn89IHuQ5dfg9pArxfHZWgu9lMdlOykVx0I6OXkE35A/mFqwwApyiPmiwno"""
73+
"""jmRnN//pApl6QQFINHzV/PGOSi599F1Y2tHQwcdb44CPOhkUmHtC9wKazSvw/ivbxNjcMzhhHsWGnA=="""
74+
strict=True, disallow_options=True)
75+
for key in key_file.keys:
76+
print(key.key_type, key.bits, key.hash_512())
77+
78+
6279
Options
6380
-------
6481

sshpubkeys/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from .keys import * # pylint:disable=wildcard-import
21
from .exceptions import * # pylint:disable=wildcard-import
2+
from .keys import * # pylint:disable=wildcard-import

sshpubkeys/keys.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,36 @@
2727
from cryptography.hazmat.primitives.asymmetric.dsa import DSAParameterNumbers, DSAPublicNumbers
2828
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
2929

30-
from .exceptions import * # pylint:disable=wildcard-import,unused-wildcard-import
30+
from .exceptions import (InvalidKeyError, InvalidKeyLengthError, InvalidOptionNameError, InvalidOptionsError,
31+
InvalidTypeError, MalformedDataError, MissingMandatoryOptionValueError, TooLongKeyError,
32+
TooShortKeyError, UnknownOptionNameError)
3133

32-
__all__ = ["SSHKey"]
34+
__all__ = ["AuthorizedKeysFile", "SSHKey"]
35+
36+
37+
class AuthorizedKeysFile(object): # pylint:disable=too-few-public-methods
38+
"""Represents a full authorized_keys file.
39+
40+
Comments and empty lines are ignored."""
41+
42+
def __init__(self, file_obj, **kwargs):
43+
self.keys = []
44+
self.parse(file_obj, **kwargs)
45+
46+
def parse(self, file_obj, **kwargs):
47+
for line in file_obj:
48+
line = line.strip()
49+
if not line:
50+
continue
51+
if line.startswith("#"):
52+
continue
53+
ssh_key = SSHKey(line, **kwargs)
54+
ssh_key.parse()
55+
self.keys.append(ssh_key)
3356

3457

3558
class SSHKey(object): # pylint:disable=too-many-instance-attributes
36-
"""Presents a single SSH keypair.
59+
"""Represents a single SSH keypair.
3760
3861
ssh_key = SSHKey(key_data, strict=True)
3962
ssh_key.parse()
@@ -110,6 +133,9 @@ def __init__(self, keydata=None, **kwargs):
110133
except (InvalidKeyError, NotImplementedError):
111134
pass
112135

136+
def __str__(self):
137+
return "Key type: %s, bits: %s, options: %s" % (self.key_type, self.bits, self.options)
138+
113139
def reset(self):
114140
"""Reset all data fields."""
115141
for field in self.FIELDS:

tests/__init__.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
55
"""
66

7+
import sys
78
import unittest
8-
from sshpubkeys import SSHKey
9+
10+
from sshpubkeys import AuthorizedKeysFile, InvalidOptionsError, SSHKey
11+
12+
from .authorized_keys import items as list_of_authorized_keys
13+
from .invalid_authorized_keys import items as list_of_invalid_authorized_keys
14+
from .invalid_keys import keys as list_of_invalid_keys
15+
from .invalid_options import options as list_of_invalid_options
916
from .valid_keys import keys as list_of_valid_keys
1017
from .valid_keys_rfc4716 import keys as list_of_valid_keys_rfc4716
11-
from .invalid_keys import keys as list_of_invalid_keys
1218
from .valid_options import options as list_of_valid_options
13-
from .invalid_options import options as list_of_invalid_options
19+
20+
if sys.version_info.major == 2:
21+
from io import BytesIO as StringIO
22+
else:
23+
from io import StringIO
24+
25+
26+
DEFAULT_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEGODBKRjsFB/1v3pDRGpA6xR+QpOJg9vat0brlbUNDD"
1427

1528

1629
class TestMisc(unittest.TestCase):
@@ -52,6 +65,32 @@ def check_invalid_option(self, option, expected_error):
5265
ssh = SSHKey()
5366
self.assertRaises(expected_error, ssh.parse_options, option)
5467

68+
def test_disallow_options(self):
69+
ssh = SSHKey(disallow_options=True)
70+
key = """command="dump /home",no-pty,no-port-forwarding """ + DEFAULT_KEY
71+
self.assertRaises(InvalidOptionsError, ssh.parse, key)
72+
73+
74+
class TestAuthorizedKeys(unittest.TestCase):
75+
76+
def check_valid_file(self, file_str, valid_keys_count):
77+
file_obj = StringIO(file_str)
78+
key_file = AuthorizedKeysFile(file_obj)
79+
for item in key_file.keys:
80+
self.assertIsInstance(item, SSHKey)
81+
self.assertEqual(len(key_file.keys), valid_keys_count)
82+
83+
def check_invalid_file(self, file_str, expected_error):
84+
file_obj = StringIO(file_str)
85+
self.assertRaises(expected_error, AuthorizedKeysFile, file_obj)
86+
87+
def test_disallow_options(self):
88+
file_obj = StringIO("""command="dump /home",no-pty,no-port-forwarding """ + DEFAULT_KEY)
89+
self.assertRaises(InvalidOptionsError, AuthorizedKeysFile, file_obj, disallow_options=True)
90+
file_obj.seek(0)
91+
key_file = AuthorizedKeysFile(file_obj)
92+
self.assertEqual(len(key_file.keys), 1)
93+
5594

5695
def loop_options(options):
5796
""" Loop over list of options and dynamically create tests """
@@ -106,11 +145,29 @@ def ch(pubkey, expected_error, **kwargs):
106145
setattr(TestKeys, "test_%s_mode_%s" % (prefix_tmp, mode), ch(pubkey, expected_error, **kwargs))
107146

108147

148+
def loop_authorized_keys(keyset):
149+
def ch(file_str, valid_keys_count):
150+
return lambda self: self.check_valid_file(file_str, valid_keys_count)
151+
for i, items in enumerate(keyset):
152+
prefix_tmp = "%s_%s" % (items[0], i)
153+
setattr(TestAuthorizedKeys, "test_%s" % prefix_tmp, ch(items[1], items[2]))
154+
155+
156+
def loop_invalid_authorized_keys(keyset):
157+
def ch(file_str, expected_error, **kwargs):
158+
return lambda self: self.check_invalid_file(file_str, expected_error, **kwargs)
159+
for i, items in enumerate(keyset):
160+
prefix_tmp = "%s_%s" % (items[0], i)
161+
setattr(TestAuthorizedKeys, "test_invalid_%s" % prefix_tmp, ch(items[1], items[2]))
162+
163+
109164
loop_valid(list_of_valid_keys, "valid_key")
110165
loop_valid(list_of_valid_keys_rfc4716, "valid_key_rfc4716")
111166
loop_invalid(list_of_invalid_keys, "invalid_key")
112167
loop_options(list_of_valid_options)
113168
loop_invalid_options(list_of_invalid_options)
169+
loop_authorized_keys(list_of_authorized_keys)
170+
loop_invalid_authorized_keys(list_of_invalid_authorized_keys)
114171

115172
if __name__ == '__main__':
116173
unittest.main()

tests/authorized_keys.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from .valid_keys import keys
2+
3+
items = [
4+
["empty_file", "", 0],
5+
["single_key", keys[0][0], 1],
6+
["comment_only", "# Nothing else than a comment here", 0],
7+
["lines_with_spaces", " # Comments\n \n" + keys[0][0] + "\n#asdf", 1],
8+
]

tests/invalid_authorized_keys.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from sshpubkeys.exceptions import InvalidKeyError, MalformedDataError
2+
3+
from .valid_keys import keys as valid_keys
4+
5+
from.invalid_keys import keys as invalid_keys
6+
7+
items = [
8+
["lines_with_spaces", " # Comments\n \n" + valid_keys[0][0] + "\nasdf", InvalidKeyError],
9+
["invalid_key", "# Comments\n" + invalid_keys[0][0], MalformedDataError],
10+
]

0 commit comments

Comments
 (0)