| 1 | #! /usr/bin/env python |
| 2 | |
| 3 | # $Id: kh2reg.py,v 1.3 2003/10/21 13:26:12 jacob Exp $ |
| 4 | # Convert OpenSSH known_hosts and known_hosts2 files to "new format" PuTTY |
| 5 | # host keys. |
| 6 | # usage: |
| 7 | # kh2reg.py [ --win ] known_hosts1 2 3 4 ... > hosts.reg |
| 8 | # Creates a Windows .REG file (double-click to install). |
| 9 | # kh2reg.py --unix known_hosts1 2 3 4 ... > sshhostkeys |
| 10 | # Creates data suitable for storing in ~/.putty/sshhostkeys (Unix). |
| 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 |
| 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 | 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 |
| 63 | |
| 64 | [HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys] |
| 65 | """) |
| 66 | |
| 67 | # Now process all known_hosts input. |
| 68 | for line in fileinput.input(args): |
| 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 | |
| 90 | # Treat as SSH1-type host key. |
| 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 | |
| 98 | # Treat as SSH2-type host key. |
| 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: |
| 132 | # Slightly bizarre key format: 'type@port:hostname' |
| 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), ',') |
| 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)) |
| 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 |