X-Git-Url: https://asedeno.scripts.mit.edu/gitweb/?a=blobdiff_plain;f=contrib%2Fkh2reg.py;h=5f9463d0718256c87ee4a933616f5a9226571e3d;hb=24da181a01ecd851757549afd20978e0bfac445b;hp=7904d65eb702ec4d934712289e2375641da3e5d8;hpb=56a42d09d43e91603f3fbf01f5781bcbbc54a6bd;p=PuTTY.git diff --git a/contrib/kh2reg.py b/contrib/kh2reg.py index 7904d65e..5f9463d0 100755 --- a/contrib/kh2reg.py +++ b/contrib/kh2reg.py @@ -36,6 +36,11 @@ def strtolong(s): 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. @@ -47,6 +52,11 @@ def longtohex(n): 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: @@ -68,8 +78,12 @@ class BlankInputLine(Exception): pass class UnknownKeyType(Exception): - def __init__(self, keytype): - self.keytype = keytype + 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): @@ -87,7 +101,7 @@ for line in fileinput.input(args): # Common fields hostpat = fields[0] - magicnumbers = [] # placeholder + keyparams = [] # placeholder keytype = "" # placeholder # Grotty heuristic to distinguish known_hosts from known_hosts2: @@ -97,7 +111,7 @@ for line in fileinput.input(args): # 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: @@ -118,25 +132,100 @@ for line in fileinput.input(args): 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]) + + 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 - # Translate key type into something PuTTY can use. - if sshkeytype == "ssh-rsa": keytype = "rsa2" - elif sshkeytype == "ssh-dss": keytype = "dss" + keyparams = [x, y] else: 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): - sys.stderr.write("Skipping hashed hostname '%s'\n" % host) + warn("skipping hashed hostname '%s'" % host) continue else: m = re.match (r"\[([^]]*)\]:(\d*)$", host) @@ -148,7 +237,11 @@ for line in fileinput.input(args): # 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)) @@ -159,6 +252,8 @@ for line in fileinput.input(args): % (winmungestr(key), value)) except UnknownKeyType, k: - sys.stderr.write("Unknown SSH key type '%s', skipping\n" % k.keytype) + warn("unknown SSH key type '%s', skipping" % k.keytype) + except KeyFormatError, k: + warn("trouble parsing key (%s), skipping" % k.msg) except BlankInputLine: pass