3 # Convert OpenSSH known_hosts and known_hosts2 files to "new format" PuTTY
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
23 "Duplicate of PuTTY's mungestr() in winstore.c:1.10 for Registry keys"
27 if c in ' \*?%~' or ord(c)<ord(' ') or (c == '.' and not candot):
28 r = r + ("%%%02X" % ord(c))
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)
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)
45 """Convert long int to lower-case hex.
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."""
52 plain=string.lower(re.match(r"0x([0-9A-Fa-f]*)l?$", hex(n), re.I).group(1))
56 "Warning with file/line number"
57 sys.stderr.write("%s:%d: %s\n"
58 % (fileinput.filename(), fileinput.filelineno(), s))
60 output_type = 'windows'
63 optlist, args = getopt.getopt(sys.argv[1:], '', [ 'win', 'unix' ])
64 if filter(lambda x: x[0] == '--unix', optlist):
66 except getopt.error, e:
67 sys.stderr.write(str(e) + "\n")
70 if output_type == 'windows':
71 # Output REG file header.
72 sys.stdout.write("""REGEDIT4
74 [HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys]
77 class BlankInputLine(Exception):
80 class UnknownKeyType(Exception):
81 def __init__(self, keytype):
82 self.keytype = keytype
84 class KeyFormatError(Exception):
85 def __init__(self, msg):
88 # Now process all known_hosts input.
89 for line in fileinput.input(args):
92 # Remove leading/trailing whitespace (should zap CR and LF)
93 line = string.strip (line)
95 # Skip blanks and comments
96 if line == '' or line[0] == '#':
99 # Split line on spaces.
100 fields = string.split (line, ' ')
104 keyparams = [] # placeholder
105 keytype = "" # placeholder
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]):
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])
119 # Treat as SSH-2-type host key.
120 # Format: hostpat keytype keyblob64 comment...
121 sshkeytype, blob = fields[1], base64.decodestring (fields[2])
123 # 'blob' consists of a number of
124 # uint32 N (big-endian)
125 # uint8[N] field_data
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 : ]
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]))
141 # Translate key type string into something PuTTY can use, and
142 # munge the rest of the data.
143 if sshkeytype == "ssh-rsa":
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:])
149 elif sshkeytype == "ssh-dss":
152 keyparams = map (strtolong, subfields[1:])
154 elif sshkeytype == "ecdsa-sha2-nistp256" \
155 or sshkeytype == "ecdsa-sha2-nistp384" \
156 or sshkeytype == "ecdsa-sha2-nistp521":
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),
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).
175 raise KeyFormatError("odd-length X+Y")
177 (x,y) = Q[1:bnlen+1], Q[bnlen+1:2*bnlen+1]
178 keyparams = [curvename] + map (strtolong, [x,y])
180 elif sshkeytype == "ssh-ed25519":
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
191 y = strtolong_le(subfields[1])
195 # Standard Ed25519 parameters.
197 d = 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3
199 # Recover x^2 = (y^2 - 1) / (d y^2 + 1).
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
206 xx = (y*y - 1) * pow(d*y*y + 1, p-2, p) % p
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
214 # Pick the square root of the correct parity.
215 if (x % 2) != x_parity:
220 raise UnknownKeyType(sshkeytype)
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)
227 elif re.match (r"\|", host):
228 warn("skipping hashed hostname '%s'" % host)
231 m = re.match (r"\[([^]]*)\]:(\d*)$", host)
233 (host, port) = m.group(1,2)
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),
245 if output_type == 'unix':
247 sys.stdout.write('%s %s\n' % (key, value))
250 # XXX: worry about double quotes?
251 sys.stdout.write("\"%s\"=\"%s\"\n"
252 % (winmungestr(key), value))
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: