#! /usr/bin/env python
-# $Id: kh2reg.py,v 1.2 2003/10/14 23:23:28 jacob Exp $
# Convert OpenSSH known_hosts and known_hosts2 files to "new format" PuTTY
# host keys.
# usage:
-# kh2reg.py [ -win ] known_hosts1 2 3 4 ... > hosts.reg
+# kh2reg.py [ --win ] known_hosts1 2 3 4 ... > hosts.reg
# Creates a Windows .REG file (double-click to install).
-# kh2reg.py -unix known_hosts1 2 3 4 ... > sshhostkeys
+# kh2reg.py --unix known_hosts1 2 3 4 ... > sshhostkeys
# Creates data suitable for storing in ~/.putty/sshhostkeys (Unix).
# Line endings are someone else's problem as is traditional.
-# Developed for Python 1.5.2.
+# Originally developed for Python 1.5.2, but probably won't run on that
+# any more.
import fileinput
import base64
bytes = struct.unpack(">%luB" % len(s), s)
return reduce ((lambda a, b: (long(a) << 8) + long(b)), bytes)
+def strtolong_le(s):
+ "Convert arbitrary-length little-endian binary data to a Python long"
+ bytes = reversed(struct.unpack(">%luB" % len(s), s))
+ return reduce ((lambda a, b: (long(a) << 8) + long(b)), bytes)
+
def longtohex(n):
"""Convert long int to lower-case hex.
plain=string.lower(re.match(r"0x([0-9A-Fa-f]*)l?$", hex(n), re.I).group(1))
return "0x" + plain
+def warn(s):
+ "Warning with file/line number"
+ sys.stderr.write("%s:%d: %s\n"
+ % (fileinput.filename(), fileinput.filelineno(), s))
+
output_type = 'windows'
try:
[HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys]
""")
+class BlankInputLine(Exception):
+ pass
+
+class UnknownKeyType(Exception):
+ def __init__(self, keytype):
+ self.keytype = keytype
+
+class KeyFormatError(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
# Now process all known_hosts input.
for line in fileinput.input(args):
# Skip blanks and comments
if line == '' or line[0] == '#':
- raise "Skipping input line"
+ raise BlankInputLine
# Split line on spaces.
fields = string.split (line, ' ')
# Common fields
hostpat = fields[0]
- magicnumbers = [] # placeholder
+ keyparams = [] # placeholder
keytype = "" # placeholder
# Grotty heuristic to distinguish known_hosts from known_hosts2:
# is second field entirely decimal digits?
if re.match (r"\d*$", fields[1]):
- # Treat as SSH1-type host key.
+ # Treat as SSH-1-type host key.
# Format: hostpat bits10 exp10 mod10 comment...
# (PuTTY doesn't store the number of bits.)
- magicnumbers = map (long, fields[2:4])
+ keyparams = map (long, fields[2:4])
keytype = "rsa"
else:
- # Treat as SSH2-type host key.
+ # Treat as SSH-2-type host key.
# Format: hostpat keytype keyblob64 comment...
sshkeytype, blob = fields[1], base64.decodestring (fields[2])
subfields.append(data)
blob = blob [struct.calcsize(sizefmt) + size : ]
- # The first field is keytype again, and the rest we can treat as
- # an opaque list of bignums (same numbers and order as stored
- # by PuTTY). (currently embedded keytype is ignored entirely)
- magicnumbers = map (strtolong, subfields[1:])
+ # The first field is keytype again.
+ if subfields[0] != sshkeytype:
+ raise KeyFormatError("""
+ outer and embedded key types do not match: '%s', '%s'
+ """ % (sshkeytype, subfields[1]))
+
+ # Translate key type string into something PuTTY can use, and
+ # munge the rest of the data.
+ if sshkeytype == "ssh-rsa":
+ keytype = "rsa2"
+ # The rest of the subfields we can treat as an opaque list
+ # of bignums (same numbers and order as stored by PuTTY).
+ keyparams = map (strtolong, subfields[1:])
+
+ elif sshkeytype == "ssh-dss":
+ keytype = "dss"
+ # Same again.
+ keyparams = map (strtolong, subfields[1:])
+
+ elif sshkeytype == "ecdsa-sha2-nistp256" \
+ or sshkeytype == "ecdsa-sha2-nistp384" \
+ or sshkeytype == "ecdsa-sha2-nistp521":
+ keytype = sshkeytype
+ # Have to parse this a bit.
+ if len(subfields) > 3:
+ raise KeyFormatError("too many subfields in blob")
+ (curvename, Q) = subfields[1:]
+ # First is yet another copy of the key name.
+ if not re.match("ecdsa-sha2-" + re.escape(curvename),
+ sshkeytype):
+ raise KeyFormatError("key type mismatch ('%s' vs '%s')"
+ % (sshkeytype, curvename))
+ # Second contains key material X and Y (hopefully).
+ # First a magic octet indicating point compression.
+ if struct.unpack("B", Q[0])[0] != 4:
+ # No-one seems to use this.
+ raise KeyFormatError("can't convert point-compressed ECDSA")
+ # Then two equal-length bignums (X and Y).
+ bnlen = len(Q)-1
+ if (bnlen % 1) != 0:
+ raise KeyFormatError("odd-length X+Y")
+ bnlen = bnlen / 2
+ (x,y) = Q[1:bnlen+1], Q[bnlen+1:2*bnlen+1]
+ keyparams = [curvename] + map (strtolong, [x,y])
- # Translate key type into something PuTTY can use.
- if sshkeytype == "ssh-rsa": keytype = "rsa2"
- elif sshkeytype == "ssh-dss": keytype = "dss"
+ elif sshkeytype == "ssh-ed25519":
+ keytype = sshkeytype
+
+ if len(subfields) != 2:
+ raise KeyFormatError("wrong number of subfields in blob")
+ if subfields[0] != sshkeytype:
+ raise KeyFormatError("key type mismatch ('%s' vs '%s')"
+ % (sshkeytype, subfields[0]))
+ # Key material y, with the top bit being repurposed as
+ # the expected parity of the associated x (point
+ # compression).
+ y = strtolong_le(subfields[1])
+ x_parity = y >> 255
+ y &= ~(1 << 255)
+
+ # Standard Ed25519 parameters.
+ p = 2**255 - 19
+ d = 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3
+
+ # Recover x^2 = (y^2 - 1) / (d y^2 + 1).
+ #
+ # With no real time constraints here, it's easier to
+ # take the inverse of the denominator by raising it to
+ # the power p-2 (by Fermat's Little Theorem) than
+ # faffing about with the properly efficient Euclid
+ # method.
+ xx = (y*y - 1) * pow(d*y*y + 1, p-2, p) % p
+
+ # Take the square root, which may require trying twice.
+ x = pow(xx, (p+3)/8, p)
+ if pow(x, 2, p) != xx:
+ x = x * pow(2, (p-1)/4, p) % p
+ assert pow(x, 2, p) == xx
+
+ # Pick the square root of the correct parity.
+ if (x % 2) != x_parity:
+ x = p - x
+
+ keyparams = [x, y]
else:
- raise "Unknown SSH key type", sshkeytype
+ raise UnknownKeyType(sshkeytype)
# Now print out one line per host pattern, discarding wildcards.
for host in string.split (hostpat, ','):
if re.search (r"[*?!]", host):
- sys.stderr.write("Skipping wildcard host pattern '%s'\n"
- % host)
+ warn("skipping wildcard host pattern '%s'" % host)
+ continue
+ elif re.match (r"\|", host):
+ warn("skipping hashed hostname '%s'" % host)
continue
else:
- # Slightly bizarre key format: 'type@port:hostname'
- # As far as I know, the input never specifies a port.
- port = 22
+ m = re.match (r"\[([^]]*)\]:(\d*)$", host)
+ if m:
+ (host, port) = m.group(1,2)
+ port = int(port)
+ else:
+ port = 22
+ # Slightly bizarre output key format: 'type@port:hostname'
# XXX: does PuTTY do anything useful with literal IP[v4]s?
key = keytype + ("@%d:%s" % (port, host))
- value = string.join (map (longtohex, magicnumbers), ',')
+ # Most of these are numbers, but there's the occasional
+ # string that needs passing through
+ value = string.join (map (
+ lambda x: x if isinstance(x, basestring) else longtohex(x),
+ keyparams), ',')
if output_type == 'unix':
# Unix format.
sys.stdout.write('%s %s\n' % (key, value))
sys.stdout.write("\"%s\"=\"%s\"\n"
% (winmungestr(key), value))
- except "Unknown SSH key type", k:
- sys.stderr.write("Unknown SSH key type '%s', skipping\n" % k)
- except "Skipping input line":
+ except UnknownKeyType, k:
+ warn("unknown SSH key type '%s', skipping" % k.keytype)
+ except KeyFormatError, k:
+ warn("trouble parsing key (%s), skipping" % k.msg)
+ except BlankInputLine:
pass