]> asedeno.scripts.mit.edu Git - PuTTY.git/blob - contrib/kh2reg.py
Partial update of kh2reg.py for new formats.
[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 longtohex(n):
40     """Convert long int to lower-case hex.
41
42     Ick, Python (at least in 1.5.2) doesn't appear to have a way to
43     turn a long int into an unadorned hex string -- % gets upset if the
44     number is too big, and raw hex() uses uppercase (sometimes), and
45     adds unwanted "0x...L" around it."""
46
47     plain=string.lower(re.match(r"0x([0-9A-Fa-f]*)l?$", hex(n), re.I).group(1))
48     return "0x" + plain
49
50 def warn(s):
51     "Warning with file/line number"
52     sys.stderr.write("%s:%d: %s\n"
53                      % (fileinput.filename(), fileinput.filelineno(), s))
54
55 output_type = 'windows'
56
57 try:
58     optlist, args = getopt.getopt(sys.argv[1:], '', [ 'win', 'unix' ])
59     if filter(lambda x: x[0] == '--unix', optlist):
60         output_type = 'unix'
61 except getopt.error, e:
62     sys.stderr.write(str(e) + "\n")
63     sys.exit(1)
64
65 if output_type == 'windows':
66     # Output REG file header.
67     sys.stdout.write("""REGEDIT4
68
69 [HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys]
70 """)
71
72 class BlankInputLine(Exception):
73     pass
74
75 class UnknownKeyType(Exception):
76     def __init__(self, keytype):
77         self.keytype = keytype
78
79 class KeyFormatError(Exception):
80     def __init__(self, msg):
81         self.msg = msg
82
83 # Now process all known_hosts input.
84 for line in fileinput.input(args):
85
86     try:
87         # Remove leading/trailing whitespace (should zap CR and LF)
88         line = string.strip (line)
89
90         # Skip blanks and comments
91         if line == '' or line[0] == '#':
92             raise BlankInputLine
93
94         # Split line on spaces.
95         fields = string.split (line, ' ')
96
97         # Common fields
98         hostpat = fields[0]
99         keyparams = []      # placeholder
100         keytype = ""        # placeholder
101
102         # Grotty heuristic to distinguish known_hosts from known_hosts2:
103         # is second field entirely decimal digits?
104         if re.match (r"\d*$", fields[1]):
105
106             # Treat as SSH-1-type host key.
107             # Format: hostpat bits10 exp10 mod10 comment...
108             # (PuTTY doesn't store the number of bits.)
109             keyparams = map (long, fields[2:4])
110             keytype = "rsa"
111
112         else:
113
114             # Treat as SSH-2-type host key.
115             # Format: hostpat keytype keyblob64 comment...
116             sshkeytype, blob = fields[1], base64.decodestring (fields[2])
117
118             # 'blob' consists of a number of
119             #   uint32    N (big-endian)
120             #   uint8[N]  field_data
121             subfields = []
122             while blob:
123                 sizefmt = ">L"
124                 (size,) = struct.unpack (sizefmt, blob[0:4])
125                 size = int(size)   # req'd for slicage
126                 (data,) = struct.unpack (">%lus" % size, blob[4:size+4])
127                 subfields.append(data)
128                 blob = blob [struct.calcsize(sizefmt) + size : ]
129
130             # The first field is keytype again.
131             if subfields[0] != sshkeytype:
132                 raise KeyFormatError("""
133                     outer and embedded key types do not match: '%s', '%s'
134                     """ % (sshkeytype, subfields[1]))
135
136             # Translate key type string into something PuTTY can use, and
137             # munge the rest of the data.
138             if sshkeytype == "ssh-rsa":
139                 keytype = "rsa2"
140                 # The rest of the subfields we can treat as an opaque list
141                 # of bignums (same numbers and order as stored by PuTTY).
142                 keyparams = map (strtolong, subfields[1:])
143
144             elif sshkeytype == "ssh-dss":
145                 keytype = "dss"
146                 # Same again.
147                 keyparams = map (strtolong, subfields[1:])
148
149             elif sshkeytype == "ecdsa-sha2-nistp256" \
150               or sshkeytype == "ecdsa-sha2-nistp384" \
151               or sshkeytype == "ecdsa-sha2-nistp521":
152                 keytype = sshkeytype
153                 # Have to parse this a bit.
154                 if len(subfields) > 3:
155                     raise KeyFormatError("too many subfields in blob")
156                 (curvename, Q) = subfields[1:]
157                 # First is yet another copy of the key name.
158                 if not re.match("ecdsa-sha2-" + re.escape(curvename),
159                                 sshkeytype):
160                     raise KeyFormatError("key type mismatch ('%s' vs '%s')"
161                             % (sshkeytype, curvename))
162                 # Second contains key material X and Y (hopefully).
163                 # First a magic octet indicating point compression.
164                 if struct.unpack("B", Q[0])[0] != 4:
165                     # No-one seems to use this.
166                     raise KeyFormatError("can't convert point-compressed ECDSA")
167                 # Then two equal-length bignums (X and Y).
168                 bnlen = len(Q)-1
169                 if (bnlen % 1) != 0:
170                     raise KeyFormatError("odd-length X+Y")
171                 bnlen = bnlen / 2
172                 (x,y) = Q[1:bnlen+1], Q[bnlen+1:2*bnlen+1]
173                 keyparams = [curvename] + map (strtolong, [x,y])
174
175             elif sshkeytype == "ssh-ed25519":
176                 # FIXME: these are always stored point-compressed, which
177                 # requires actual maths
178                 raise KeyFormatError("can't convert ssh-ed25519 yet, sorry")
179
180             else:
181                 raise UnknownKeyType(sshkeytype)
182
183         # Now print out one line per host pattern, discarding wildcards.
184         for host in string.split (hostpat, ','):
185             if re.search (r"[*?!]", host):
186                 warn("skipping wildcard host pattern '%s'" % host)
187                 continue
188             elif re.match (r"\|", host):
189                 warn("skipping hashed hostname '%s'" % host)
190                 continue
191             else:
192                 m = re.match (r"\[([^]]*)\]:(\d*)$", host)
193                 if m:
194                     (host, port) = m.group(1,2)
195                     port = int(port)
196                 else:
197                     port = 22
198                 # Slightly bizarre output key format: 'type@port:hostname'
199                 # XXX: does PuTTY do anything useful with literal IP[v4]s?
200                 key = keytype + ("@%d:%s" % (port, host))
201                 # Most of these are numbers, but there's the occasional
202                 # string that needs passing through
203                 value = string.join (map (
204                     lambda x: x if isinstance(x, basestring) else longtohex(x),
205                     keyparams), ',')
206                 if output_type == 'unix':
207                     # Unix format.
208                     sys.stdout.write('%s %s\n' % (key, value))
209                 else:
210                     # Windows format.
211                     # XXX: worry about double quotes?
212                     sys.stdout.write("\"%s\"=\"%s\"\n"
213                                      % (winmungestr(key), value))
214
215     except UnknownKeyType, k:
216         warn("unknown SSH key type '%s', skipping" % k.keytype)
217     except KeyFormatError, k:
218         warn("trouble parsing key (%s), skipping" % k.msg)
219     except BlankInputLine:
220         pass