aboutsummaryrefslogtreecommitdiff
path: root/gitosis/sshkey.py
blob: 145d74627cb00ded5075a408b909d17744ad3512 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""
Gitosis code to intelligently handle SSH public keys.
 
"""
from shlex import shlex
from StringIO import StringIO
import re
 
# The 'ecc' and 'ecdh' types are speculative, based on the Internet Draft 
# http://www.ietf.org/internet-drafts/draft-green-secsh-ecc-02.txt 
SSH_KEY_PROTO2_TYPES = ['ssh-dsa', 
                        'ssh-dss', 
                        'ssh-ecc', 
                        'ssh-ecdh', 
                        'ssh-rsa']
 
# These options must not have arguments 
SSH_KEY_OPTS = ['no-agent-forwarding', 
                'no-port-forwarding', 
                'no-pty', 
                'no-X11-forwarding']
# These options require arguments 
SSH_KEY_OPTS_WITH_ARGS = ['command', 
                          'environment', 
                          'from', 
                          'permitopen', 
                          'tunnel' ]
 
class MalformedSSHKey(Exception):
    """Malformed SSH public key"""
 
class InsecureSSHKeyUsername(Exception):
    """Username contains not allowed characters"""
    def __str__(self):
        return '%s%s' % (self.__doc__''.join(self.args))
 
class SSHPublicKey:
    """Base class for representing an SSH public key"""
    def __init__(self, opts, keydata, comment):
        """Create a new instance."""
        self._options = opts
        self._comment = comment
        self._username = comment
        _ = comment.split(None)
        if len(_) > 1:
            self._username = _[0]
        _ = keydata
 
    @property
    def options(self):
        """Returns a dictionary of options used with the SSH public key."""
        return self._options
 
    @property
    def comment(self):
        """Returns the comment associated with the SSH public key."""
        return self._comment
 
    @property
    def username(self):
        """
        Returns the username from the comment, the first word of the comment.
        """
        if isSafeUsername(self._username):
            return self._username
        else:
            raise InsecureSSHKeyUsername(repr(self._username))
    
    def options_string(self):
        """Return the options array as a suitable string."""
        def _single_option():
            """Convert a single option to a usable string."""
            for (keyval) in self._options.items():
                _ = key
                if val is not None:
                    _ += "=\"%s\"" % (val.replace('"''\\"'))
                yield _
        return ','.join(_single_option())
 
    @property
    def key(self):
        """Abstract method"""
        raise NotImplementedError()
 
    @property
    def full_key(self):
        """Return a full SSH public key line, as found in authorized_keys"""
        options = self.options_string()
        if len(options) > 0:
            options += ' '
        return '%s%s %s' % (optionsself.keyself.comment)
 
    def __str__(self):
        return self.full_key
 
class SSH1PublicKey(SSHPublicKey):
    """Class for representing an SSH public key, protocol version 1"""
    def __init__(self, opts, keydata, comment):
        """Create a new instance."""
        SSHPublicKey.__init__(selfoptskeydatacomment)
        (self._key_bits
        self._key_exponent
        self._key_modulus) = keydata.split(' ')
    @property
    def key(self):
        """Return just the SSH key data, without options or comments."""
        return '%s %s %s' % (self._key_bits
                             self._key_exponent
                             self._key_modulus)
 
class SSH2PublicKey(SSHPublicKey):
    """Class for representing an SSH public key, protocol version 2"""
    def __init__(self, opts, keydata, comment):
        """Create a new instance."""
        SSHPublicKey.__init__(selfoptskeydatacomment)
        (self._key_prefixself._key_base64) = keydata.split(' ')
    @property
    def key(self):
        """Return just the SSH key data, without options or comments."""
        return '%s %s' % (self._key_prefixself._key_base64)
 
def get_ssh_pubkey(line):
    """Take an SSH public key, and return an object representing it."""
    (optskeydatacomment) = _explode_ssh_key(line)
    if keydata.startswith('ssh-'):
        return SSH2PublicKey(optskeydatacomment)
    else:
        return SSH1PublicKey(optskeydatacomment)
 
def _explode_ssh_key(line):
    """
    Break apart a public-key line correct.
    - Protocol 1 public keys consist of: 
      options, bits, exponent, modulus, comment. 
    - Protocol 2 public key consist of: 
      options, keytype, base64-encoded key, comment.
    - For all options that take an argument, having a quote inside the argument
      is valid, and should be in the file as '\"'
    - Spaces are also valid in those arguments.
    - Options must be seperated by commas.
    Seperately return the options, key data and comment.
    """
    opts = {}
    shl = shlex(StringIO(line)NoneTrue)
    shl.wordchars += '-'
    # Treat ',' as whitespace seperation the options 
    shl.whitespace += ','
    shl.whitespace_split = 1
    # Handle the options first 
    keydata = None
    def _check_eof(tok):
        """See if the end was nigh."""
        if tok == shl.eof:
            raise MalformedSSHKey("Unexpected end of key")
    while True:
        tok = shl.get_token()
        _check_eof(tok)
        # This is the start of the actual key, protocol 1 
        if tok.isdigit():
            keydata = tok
            expected_key_args = 2
            break
        # This is the start of the actual key, protocol 2 
        if tok in SSH_KEY_PROTO2_TYPES:
            keydata = tok
            expected_key_args = 1
            break
        if tok in SSH_KEY_OPTS:
            opts[tok] = None
            continue
        if '=' in tok:
            (tok_) = tok.split('='1)
            if tok in SSH_KEY_OPTS_WITH_ARGS:
                opts[tok] = _
                continue
        raise MalformedSSHKey("Unknown fragment %r" % (tok))
    # Now handle the key 
    # Protocol 2 keys have only 1 argument besides the type 
    # Protocol 1 keys have 2 arguments after the bit-count. 
    shl.whitespace_split = 1
    while expected_key_args > 0:
        _ = shl.get_token()
        _check_eof(_)
        keydata += ' '+_
        expected_key_args -= 1
    # Everything that remains is a comment 
    comment = ''
    shl.whitespace = ''
    while True:
        _ = shl.get_token()
        if _ == shl.eof or _ == None:
            break
        comment += _
    return (optskeydatacomment)
 
_ACCEPTABLE_USER_RE = re.compile(
        r'^[a-zA-Z][a-zA-Z0-9_.-]*(@[a-zA-Z][a-zA-Z0-9.-]*)?$'
        )
 
def isSafeUsername(user):
    """Is the username safe to use a a filename? """
    match = _ACCEPTABLE_USER_RE.match(user)
    return (match is not None)