2d51c14f |
1 | #! /usr/bin/env python |
2 | |
2e85c969 |
3 | # $Id$ |
2d51c14f |
4 | # Convert OpenSSH known_hosts and known_hosts2 files to "new format" PuTTY |
f6390fa4 |
5 | # host keys. |
6 | # usage: |
8a646d92 |
7 | # kh2reg.py [ --win ] known_hosts1 2 3 4 ... > hosts.reg |
f6390fa4 |
8 | # Creates a Windows .REG file (double-click to install). |
8a646d92 |
9 | # kh2reg.py --unix known_hosts1 2 3 4 ... > sshhostkeys |
f6390fa4 |
10 | # Creates data suitable for storing in ~/.putty/sshhostkeys (Unix). |
2d51c14f |
11 | # Line endings are someone else's problem as is traditional. |
12 | # Developed for Python 1.5.2. |
13 | |
14 | import fileinput |
15 | import base64 |
16 | import struct |
17 | import string |
18 | import re |
19 | import sys |
f6390fa4 |
20 | import getopt |
2d51c14f |
21 | |
f6390fa4 |
22 | def winmungestr(s): |
2d51c14f |
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 | |
f6390fa4 |
50 | output_type = 'windows' |
51 | |
52 | try: |
53 | optlist, args = getopt.getopt(sys.argv[1:], '', [ 'win', 'unix' ]) |
54 | if filter(lambda x: x[0] == '--unix', optlist): |
55 | output_type = 'unix' |
56 | except getopt.error, e: |
57 | sys.stderr.write(str(e) + "\n") |
58 | sys.exit(1) |
59 | |
60 | if output_type == 'windows': |
61 | # Output REG file header. |
62 | sys.stdout.write("""REGEDIT4 |
2d51c14f |
63 | |
64 | [HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys] |
65 | """) |
66 | |
67 | # Now process all known_hosts input. |
f6390fa4 |
68 | for line in fileinput.input(args): |
2d51c14f |
69 | |
70 | try: |
71 | # Remove leading/trailing whitespace (should zap CR and LF) |
72 | line = string.strip (line) |
73 | |
74 | # Skip blanks and comments |
75 | if line == '' or line[0] == '#': |
76 | raise "Skipping input line" |
77 | |
78 | # Split line on spaces. |
79 | fields = string.split (line, ' ') |
80 | |
81 | # Common fields |
82 | hostpat = fields[0] |
83 | magicnumbers = [] # placeholder |
84 | keytype = "" # placeholder |
85 | |
86 | # Grotty heuristic to distinguish known_hosts from known_hosts2: |
87 | # is second field entirely decimal digits? |
88 | if re.match (r"\d*$", fields[1]): |
89 | |
2e85c969 |
90 | # Treat as SSH-1-type host key. |
2d51c14f |
91 | # Format: hostpat bits10 exp10 mod10 comment... |
92 | # (PuTTY doesn't store the number of bits.) |
93 | magicnumbers = map (long, fields[2:4]) |
94 | keytype = "rsa" |
95 | |
96 | else: |
97 | |
2e85c969 |
98 | # Treat as SSH-2-type host key. |
2d51c14f |
99 | # Format: hostpat keytype keyblob64 comment... |
100 | sshkeytype, blob = fields[1], base64.decodestring (fields[2]) |
101 | |
102 | # 'blob' consists of a number of |
103 | # uint32 N (big-endian) |
104 | # uint8[N] field_data |
105 | subfields = [] |
106 | while blob: |
107 | sizefmt = ">L" |
108 | (size,) = struct.unpack (sizefmt, blob[0:4]) |
109 | size = int(size) # req'd for slicage |
110 | (data,) = struct.unpack (">%lus" % size, blob[4:size+4]) |
111 | subfields.append(data) |
112 | blob = blob [struct.calcsize(sizefmt) + size : ] |
113 | |
114 | # The first field is keytype again, and the rest we can treat as |
115 | # an opaque list of bignums (same numbers and order as stored |
116 | # by PuTTY). (currently embedded keytype is ignored entirely) |
117 | magicnumbers = map (strtolong, subfields[1:]) |
118 | |
119 | # Translate key type into something PuTTY can use. |
120 | if sshkeytype == "ssh-rsa": keytype = "rsa2" |
121 | elif sshkeytype == "ssh-dss": keytype = "dss" |
122 | else: |
123 | raise "Unknown SSH key type", sshkeytype |
124 | |
125 | # Now print out one line per host pattern, discarding wildcards. |
126 | for host in string.split (hostpat, ','): |
127 | if re.search (r"[*?!]", host): |
128 | sys.stderr.write("Skipping wildcard host pattern '%s'\n" |
129 | % host) |
130 | continue |
131 | else: |
f6390fa4 |
132 | # Slightly bizarre key format: 'type@port:hostname' |
2d51c14f |
133 | # As far as I know, the input never specifies a port. |
134 | port = 22 |
135 | # XXX: does PuTTY do anything useful with literal IP[v4]s? |
136 | key = keytype + ("@%d:%s" % (port, host)) |
137 | value = string.join (map (longtohex, magicnumbers), ',') |
f6390fa4 |
138 | if output_type == 'unix': |
139 | # Unix format. |
140 | sys.stdout.write('%s %s\n' % (key, value)) |
141 | else: |
142 | # Windows format. |
143 | # XXX: worry about double quotes? |
144 | sys.stdout.write("\"%s\"=\"%s\"\n" |
145 | % (winmungestr(key), value)) |
2d51c14f |
146 | |
147 | except "Unknown SSH key type", k: |
148 | sys.stderr.write("Unknown SSH key type '%s', skipping\n" % k) |
149 | except "Skipping input line": |
150 | pass |