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 long int to lower-case hex.
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."""
47 plain=string.lower(re.match(r"0x([0-9A-Fa-f]*)l?$", hex(n), re.I).group(1))
51 "Warning with file/line number"
52 sys.stderr.write("%s:%d: %s\n"
53 % (fileinput.filename(), fileinput.filelineno(), s))
55 output_type = 'windows'
58 optlist, args = getopt.getopt(sys.argv[1:], '', [ 'win', 'unix' ])
59 if filter(lambda x: x[0] == '--unix', optlist):
61 except getopt.error, e:
62 sys.stderr.write(str(e) + "\n")
65 if output_type == 'windows':
66 # Output REG file header.
67 sys.stdout.write("""REGEDIT4
69 [HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys]
72 class BlankInputLine(Exception):
75 class UnknownKeyType(Exception):
76 def __init__(self, keytype):
77 self.keytype = keytype
79 class KeyFormatError(Exception):
80 def __init__(self, msg):
83 # Now process all known_hosts input.
84 for line in fileinput.input(args):
87 # Remove leading/trailing whitespace (should zap CR and LF)
88 line = string.strip (line)
90 # Skip blanks and comments
91 if line == '' or line[0] == '#':
94 # Split line on spaces.
95 fields = string.split (line, ' ')
99 keyparams = [] # placeholder
100 keytype = "" # placeholder
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]):
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])
114 # Treat as SSH-2-type host key.
115 # Format: hostpat keytype keyblob64 comment...
116 sshkeytype, blob = fields[1], base64.decodestring (fields[2])
118 # 'blob' consists of a number of
119 # uint32 N (big-endian)
120 # uint8[N] field_data
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 : ]
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]))
136 # Translate key type string into something PuTTY can use, and
137 # munge the rest of the data.
138 if sshkeytype == "ssh-rsa":
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:])
144 elif sshkeytype == "ssh-dss":
147 keyparams = map (strtolong, subfields[1:])
149 elif sshkeytype == "ecdsa-sha2-nistp256" \
150 or sshkeytype == "ecdsa-sha2-nistp384" \
151 or sshkeytype == "ecdsa-sha2-nistp521":
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),
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).
170 raise KeyFormatError("odd-length X+Y")
172 (x,y) = Q[1:bnlen+1], Q[bnlen+1:2*bnlen+1]
173 keyparams = [curvename] + map (strtolong, [x,y])
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")
181 raise UnknownKeyType(sshkeytype)
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)
188 elif re.match (r"\|", host):
189 warn("skipping hashed hostname '%s'" % host)
192 m = re.match (r"\[([^]]*)\]:(\d*)$", host)
194 (host, port) = m.group(1,2)
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),
206 if output_type == 'unix':
208 sys.stdout.write('%s %s\n' % (key, value))
211 # XXX: worry about double quotes?
212 sys.stdout.write("\"%s\"=\"%s\"\n"
213 % (winmungestr(key), value))
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: