]> asedeno.scripts.mit.edu Git - PuTTY.git/blob - contrib/kh2reg.py
Merge branch 'master' of ssh://tartarus.org/putty
[PuTTY.git] / contrib / kh2reg.py
1 #! /usr/bin/env python
2
3 # Convert OpenSSH known_hosts and known_hosts2 files to "new format" PuTTY
4 # host keys.
5 #   usage:
6 #     kh2reg.py [ --win ] known_hosts1 2 3 4 ... > hosts.reg
7 #       Creates a Windows .REG file (double-click to install).
8 #     kh2reg.py --unix    known_hosts1 2 3 4 ... > sshhostkeys
9 #       Creates data suitable for storing in ~/.putty/sshhostkeys (Unix).
10 # Line endings are someone else's problem as is traditional.
11 # Originally developed for Python 1.5.2, but probably won't run on that
12 # any more.
13
14 import fileinput
15 import base64
16 import struct
17 import string
18 import re
19 import sys
20 import getopt
21
22 def winmungestr(s):
23     "Duplicate of PuTTY's mungestr() in winstore.c:1.10 for Registry keys"
24     candot = 0
25     r = ""
26     for c in s:
27         if c in ' \*?%~' or ord(c)<ord(' ') or (c == '.' and not candot):
28             r = r + ("%%%02X" % ord(c))
29         else:
30             r = r + c
31         candot = 1
32     return r
33
34 def strtolong(s):
35     "Convert arbitrary-length big-endian binary data to a Python long"
36     bytes = struct.unpack(">%luB" % len(s), s)
37     return reduce ((lambda a, b: (long(a) << 8) + long(b)), bytes)
38
39 def strtolong_le(s):
40     "Convert arbitrary-length little-endian binary data to a Python long"
41     bytes = reversed(struct.unpack(">%luB" % len(s), s))
42     return reduce ((lambda a, b: (long(a) << 8) + long(b)), bytes)
43
44 def longtohex(n):
45     """Convert long int to lower-case hex.
46
47     Ick, Python (at least in 1.5.2) doesn't appear to have a way to
48     turn a long int into an unadorned hex string -- % gets upset if the
49     number is too big, and raw hex() uses uppercase (sometimes), and
50     adds unwanted "0x...L" around it."""
51
52     plain=string.lower(re.match(r"0x([0-9A-Fa-f]*)l?$", hex(n), re.I).group(1))
53     return "0x" + plain
54
55 def warn(s):
56     "Warning with file/line number"
57     sys.stderr.write("%s:%d: %s\n"
58                      % (fileinput.filename(), fileinput.filelineno(), s))
59
60 output_type = 'windows'
61
62 try:
63     optlist, args = getopt.getopt(sys.argv[1:], '', [ 'win', 'unix' ])
64     if filter(lambda x: x[0] == '--unix', optlist):
65         output_type = 'unix'
66 except getopt.error, e:
67     sys.stderr.write(str(e) + "\n")
68     sys.exit(1)
69
70 if output_type == 'windows':
71     # Output REG file header.
72     sys.stdout.write("""REGEDIT4
73
74 [HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys]
75 """)
76
77 class BlankInputLine(Exception):
78     pass
79
80 class UnknownKeyType(Exception):
81     def __init__(self, keytype):
82         self.keytype = keytype
83
84 class KeyFormatError(Exception):
85     def __init__(self, msg):
86         self.msg = msg
87
88 # Now process all known_hosts input.
89 for line in fileinput.input(args):
90
91     try:
92         # Remove leading/trailing whitespace (should zap CR and LF)
93         line = string.strip (line)
94
95         # Skip blanks and comments
96         if line == '' or line[0] == '#':
97             raise BlankInputLine
98
99         # Split line on spaces.
100         fields = string.split (line, ' ')
101
102         # Common fields
103         hostpat = fields[0]
104         keyparams = []      # placeholder
105         keytype = ""        # placeholder
106
107         # Grotty heuristic to distinguish known_hosts from known_hosts2:
108         # is second field entirely decimal digits?
109         if re.match (r"\d*$", fields[1]):
110
111             # Treat as SSH-1-type host key.
112             # Format: hostpat bits10 exp10 mod10 comment...
113             # (PuTTY doesn't store the number of bits.)
114             keyparams = map (long, fields[2:4])
115             keytype = "rsa"
116
117         else:
118
119             # Treat as SSH-2-type host key.
120             # Format: hostpat keytype keyblob64 comment...
121             sshkeytype, blob = fields[1], base64.decodestring (fields[2])
122
123             # 'blob' consists of a number of
124             #   uint32    N (big-endian)
125             #   uint8[N]  field_data
126             subfields = []
127             while blob:
128                 sizefmt = ">L"
129                 (size,) = struct.unpack (sizefmt, blob[0:4])
130                 size = int(size)   # req'd for slicage
131                 (data,) = struct.unpack (">%lus" % size, blob[4:size+4])
132                 subfields.append(data)
133                 blob = blob [struct.calcsize(sizefmt) + size : ]
134
135             # The first field is keytype again.
136             if subfields[0] != sshkeytype:
137                 raise KeyFormatError("""
138                     outer and embedded key types do not match: '%s', '%s'
139                     """ % (sshkeytype, subfields[1]))
140
141             # Translate key type string into something PuTTY can use, and
142             # munge the rest of the data.
143             if sshkeytype == "ssh-rsa":
144                 keytype = "rsa2"
145                 # The rest of the subfields we can treat as an opaque list
146                 # of bignums (same numbers and order as stored by PuTTY).
147                 keyparams = map (strtolong, subfields[1:])
148
149             elif sshkeytype == "ssh-dss":
150                 keytype = "dss"
151                 # Same again.
152                 keyparams = map (strtolong, subfields[1:])
153
154             elif sshkeytype == "ecdsa-sha2-nistp256" \
155               or sshkeytype == "ecdsa-sha2-nistp384" \
156               or sshkeytype == "ecdsa-sha2-nistp521":
157                 keytype = sshkeytype
158                 # Have to parse this a bit.
159                 if len(subfields) > 3:
160                     raise KeyFormatError("too many subfields in blob")
161                 (curvename, Q) = subfields[1:]
162                 # First is yet another copy of the key name.
163                 if not re.match("ecdsa-sha2-" + re.escape(curvename),
164                                 sshkeytype):
165                     raise KeyFormatError("key type mismatch ('%s' vs '%s')"
166                             % (sshkeytype, curvename))
167                 # Second contains key material X and Y (hopefully).
168                 # First a magic octet indicating point compression.
169                 if struct.unpack("B", Q[0])[0] != 4:
170                     # No-one seems to use this.
171                     raise KeyFormatError("can't convert point-compressed ECDSA")
172                 # Then two equal-length bignums (X and Y).
173                 bnlen = len(Q)-1
174                 if (bnlen % 1) != 0:
175                     raise KeyFormatError("odd-length X+Y")
176                 bnlen = bnlen / 2
177                 (x,y) = Q[1:bnlen+1], Q[bnlen+1:2*bnlen+1]
178                 keyparams = [curvename] + map (strtolong, [x,y])
179
180             elif sshkeytype == "ssh-ed25519":
181                 keytype = sshkeytype
182
183                 if len(subfields) != 2:
184                     raise KeyFormatError("wrong number of subfields in blob")
185                 if subfields[0] != sshkeytype:
186                     raise KeyFormatError("key type mismatch ('%s' vs '%s')"
187                             % (sshkeytype, subfields[0]))
188                 # Key material y, with the top bit being repurposed as
189                 # the expected parity of the associated x (point
190                 # compression).
191                 y = strtolong_le(subfields[1])
192                 x_parity = y >> 255
193                 y &= ~(1 << 255)
194
195                 # Standard Ed25519 parameters.
196                 p = 2**255 - 19
197                 d = 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3
198
199                 # Recover x^2 = (y^2 - 1) / (d y^2 + 1).
200                 #
201                 # With no real time constraints here, it's easier to
202                 # take the inverse of the denominator by raising it to
203                 # the power p-2 (by Fermat's Little Theorem) than
204                 # faffing about with the properly efficient Euclid
205                 # method.
206                 xx = (y*y - 1) * pow(d*y*y + 1, p-2, p) % p
207
208                 # Take the square root, which may require trying twice.
209                 x = pow(xx, (p+3)/8, p)
210                 if pow(x, 2, p) != xx:
211                     x = x * pow(2, (p-1)/4, p) % p
212                     assert pow(x, 2, p) == xx
213
214                 # Pick the square root of the correct parity.
215                 if (x % 2) != x_parity:
216                     x = p - x
217
218                 keyparams = [x, y]
219             else:
220                 raise UnknownKeyType(sshkeytype)
221
222         # Now print out one line per host pattern, discarding wildcards.
223         for host in string.split (hostpat, ','):
224             if re.search (r"[*?!]", host):
225                 warn("skipping wildcard host pattern '%s'" % host)
226                 continue
227             elif re.match (r"\|", host):
228                 warn("skipping hashed hostname '%s'" % host)
229                 continue
230             else:
231                 m = re.match (r"\[([^]]*)\]:(\d*)$", host)
232                 if m:
233                     (host, port) = m.group(1,2)
234                     port = int(port)
235                 else:
236                     port = 22
237                 # Slightly bizarre output key format: 'type@port:hostname'
238                 # XXX: does PuTTY do anything useful with literal IP[v4]s?
239                 key = keytype + ("@%d:%s" % (port, host))
240                 # Most of these are numbers, but there's the occasional
241                 # string that needs passing through
242                 value = string.join (map (
243                     lambda x: x if isinstance(x, basestring) else longtohex(x),
244                     keyparams), ',')
245                 if output_type == 'unix':
246                     # Unix format.
247                     sys.stdout.write('%s %s\n' % (key, value))
248                 else:
249                     # Windows format.
250                     # XXX: worry about double quotes?
251                     sys.stdout.write("\"%s\"=\"%s\"\n"
252                                      % (winmungestr(key), value))
253
254     except UnknownKeyType, k:
255         warn("unknown SSH key type '%s', skipping" % k.keytype)
256     except KeyFormatError, k:
257         warn("trouble parsing key (%s), skipping" % k.msg)
258     except BlankInputLine:
259         pass