-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathopenssh_putty_knownhost_converter.py
354 lines (268 loc) · 13.1 KB
/
openssh_putty_knownhost_converter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#!/usr/bin/env python3
#
# script that converts between
# openSSH 'known hosts' to what putty
# stores in the registry for its known hosts
#
#
# written by Mark Grandi, March 27, 2014
#
import binascii
import struct
import base64
import io
import sys
import argparse
import os, os.path
import collections
import re
import winreg
import socket
import math
# represents the data that would go inside the registry key for the putty known hosts
# example:
# keyName = rsa2@23:68.98.45.184
# exponent = 0x10001
# modulus: 0xaaaaa027...........
PuttyKnownhostEntry = collections.namedtuple("PuttyKnownhostEntry", ["keyName", "exponent", "modulus"])
def main(args):
''' figures out what function to call
@param args - the argument parser Namespace object we got from parse_args()
'''
if args.convert_to_openssh:
puttyToOpenSSH(args)
else:
openSSHToPutty(args)
def puttyToOpenSSH(args):
''' converts putty known hosts entries to openSSH ones
@param args - the argument parser Namespace object we got from parse_args()
'''
# open the registry key that has the Known hosts
puttyKey = None
try:
puttyKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\SimonTatham\\PuTTY\\SshHostKeys")
except OSError:
raise Exception("could not open the registry key containing the putty known host entries, are there any in there?")
# iterate over them, creating PuttyKnownhostEntry objects
puttyEntries = []
counter = 0
while True:
key = None
value = None
dataType = None
try:
key, value, dataType = winreg.EnumValue(puttyKey, counter)
except OSError:
# no more data
break
exp = value.split(",")[0]
modulus = value.split(",")[1]
puttyEntries.append(PuttyKnownhostEntry(key, exp, modulus))
counter += 1
# now go through each entry and convert it to a openssh line
openSshLines = []
for iterEntry in puttyEntries:
# parse the key name, which has the hostname or ip, port, and algorithm
# it looks like: 'rsa2@60101:mgrandi.no-ip.org'
alg = iterEntry.keyName.split("@")[0]
opensshAlg = ""
if alg == "rsa2":
opensshAlg = "ssh-rsa"
elif alg == "dsa":
opensshAlg = "ssh-dss"
secondPart = iterEntry.keyName.split("@")[1]
port = secondPart.split(":")[0] # putty always stores the port
hostOrIp = secondPart.split(":")[1]
hostName = hostOrIp
# if the user wants to resolve ip addresses, do it
# TODO: if we want ipv6 support we should somehow have an option to choose the
# ipv6 address if present from the result we get from getaddrinfo()
if args.should_resolve:
try:
addrList = socket.getaddrinfo(hostOrIp, port)
hostName = addrList[0][4][0]
except OSError as e:
print("#WARNING: error when trying to resolve '{}:{}', error: '{}'\n\n".format(hostOrIp, port, e))
hostName = hostOrIp
# now calculate the data that it stores
resultBytes = io.BytesIO()
resultBytes.write(struct.pack(">i", 7)) # write that there is a 7 byte algorithm identifier
resultBytes.write(opensshAlg.encode("utf-8")) # write algorithm identifier
expInt = int(iterEntry.exponent[2:], 16)
# how many bytes does it take to store this exponent?
numBytes = math.ceil(int(expInt).bit_length() / 8)
# write length of exponent
resultBytes.write(struct.pack(">i", numBytes))
# write exponent
resultBytes.write(struct.pack(">{}s".format(numBytes), int(expInt).to_bytes(numBytes, "big")))
modulusData = binascii.unhexlify(iterEntry.modulus[2:])
# here we have to 'add' a b'\x00' byte to the start of this, because i guess we are padding the modulus
# to match the RSA key modulus, see my comment later on in this file)
modulusData = b'\x00' + modulusData
# write length of modulus
resultBytes.write(struct.pack(">i", len(modulusData)))
# write modulus
resultBytes.write(modulusData)
# base64 it
result = base64.b64encode(resultBytes.getvalue())
# now we have the openssh line
# put it in our list
# (like [68.98.45.184]:23 ssh-rsa AAAAB3.....)
openSshLines.append("[{}]:{} {} {}".format(hostName, port, opensshAlg, result.decode("utf-8")))
for iterEntry in openSshLines:
args.output.write(iterEntry + "\n")
def openSSHToPutty(args):
''' converts OpenSSH known hosts entries to putty ones
@param args - the argument parser Namespace object we got from parse_args()
'''
# see if we can find the openssh known_hosts file
# or if the user provided us with one
knownHostsPath = os.path.expanduser("~/.ssh/known_hosts")
if args.openssh_knownhosts_file:
knownHostsPath = os.path.realpath(args.openssh_knownhosts_file)
# known_host entries are like this:
# [hostname,]ip-addr algorithm data
# github.com,192.30.252.131 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7h.......
# i tested this by having a list of the different formats a known_hosts entry can have
# matchList =
# ['github.com,192.30.252.131 ssh-rsa AAAAB3NzaC1yc2EAAAAB',
# '192.30.252.130 ssh-rsa AAAAB3NzaC1yc2E',
# '[68.98.45.184]:23 ssh-rsa AAAAB3NzaC1yc2EAAAADAQ',
# '[mgrandi.no-ip.org]:23,[68.98.45.184]:23 ssh-rsa AAAAB3Nz']
#
# def test():
# for iterStr in matchList:
# print("running {}".format(iterStr))
# try:
# print(knownHostRegex.search(iterStr).groupdict())
# except AttributeError:
# print("nomatch")
knownHostRegex = re.compile('''
^ # start of line
\[? # optional '['
(?P<hostname>[\w.-]+(?=.*,))? # (optional) Matches the hostname, only if a comma comes afterwards
\]? # optioanl ']'
:? # optional ':' (only there if a port is specified)
((?<=:)[0-9]*)? # optional port attached to the hostname, only matches if its preceeded by ':'
,? # optional comma
\[? # optional '['
(?P<ipaddr>[0-9a-fA-F.:]+) # ip address, should match ipv4 and ipv6 addresses although not tested for ipv6...
\]? # optional ']'
:? # optional ':'
(?P<port>[0-9]+)? # port
\s # space
(?P<algorithm>[a-z-]+) # algorithm name
\s # space
(?P<data>[a-zA-Z+/0-9=]+) # the data (modulo, exponent, etc), matches the standard base64 alphabet
$ # end of line
''', re.VERBOSE | re.MULTILINE | re.UNICODE)
# make sure file exists
if not os.path.isfile(knownHostsPath):
raise Exception("Unable to find known_hosts file, {} was not found!".format(knownHostsPath))
knownHostFileData = None
with open(knownHostsPath, "r", encoding="utf-8") as f:
knownHostFileData = f.read()
puttyResults = []
# go through every entry
for iterMatch in knownHostRegex.finditer(knownHostFileData):
puttyResults.append(opensshMatchToPuttyKnownhost(iterMatch))
#print("Key: {}\nValue: {},{}".format(iterMatchResult.keyName, iterMatchResult.exponent, iterMatchResult.modulus))
# now write to output
args.output.write("Windows Registry Editor Version 5.00\n\n")
args.output.write("[HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys]\n")
for iterEntry in puttyResults:
args.output.write('''"{}"="{},{}"\n'''.format(iterEntry.keyName, iterEntry.exponent, iterEntry.modulus))
def opensshMatchToPuttyKnownhost(matchObj):
'''takes in a re.Match object and converts the data inside to
a putty knownhost entry (that can be put inside regedit)
@param matchObj - a re.Match object that we got from parsing the openssh known_hosts file
@return a PuttyKnownhostEntry
'''
matchDict = matchObj.groupdict()
resultStr = ""
opensshBytes = io.BytesIO(base64.b64decode(matchDict["data"]))
# number of bytes to read to get the algorithm identifier
algIdentifierLen = struct.unpack(">i", opensshBytes.read(4))[0]
# convert the algorithm identifier to how putty understands it
opensshType = opensshBytes.read(algIdentifierLen).decode("utf-8")
puttyKeyType = ""
if opensshType == "ssh-rsa":
puttyKeyType = "rsa2"
elif opensshType == "ssh-dss":
puttyKeyType = "dsa"
else:
raise Exception("Unknown algorithm identifier! ({})".format(opensshType))
port = 22
if matchDict["port"]:
port = matchDict["port"]
# figure out keyname (for whats stored in the registry)
keyName = ""
if not matchDict["hostname"]:
keyName = "{}@{}:{}".format(matchDict["algorithm"], port, matchDict["ipaddr"])
else:
# if no hostname, use the ip address
keyName = "{}@{}:{}".format(matchDict["algorithm"], port, matchDict["hostname"])
# read in the length of the exponent
exponentLength = struct.unpack(">i", opensshBytes.read(4))[0]
#print("\texponent length is {}".format(exponentLength))
# read in exponent
exponent = int.from_bytes(opensshBytes.read(exponentLength), "big")
#print("\texponent is {}".format(exponent))
# read in length of modulus
modLength = struct.unpack(">i", opensshBytes.read(4))[0]
#print("\tmodulus length is {}".format(modLength))
# read in modulus
modulus = opensshBytes.read(modLength)
# ******************
# NOTE NOTE NOTE:
# *****************
#
# now we are ready to store it as a putty key
# apparently we get rid of the leading 0...
# because it needs to be padded to the length of the RSA key modulus
#
# from the putty docs:
#
# 4.25.7 `Requires padding on SSH-2 RSA signatures'
#
# Versions below 3.3 of OpenSSH require SSH-2 RSA signatures to be
# padded with zero bytes to the same length as the RSA key modulus.
# The SSH-2 specification says that an unpadded signature MUST be
# accepted, so this is a bug. A typical symptom of this problem is
# that PuTTY mysteriously fails RSA authentication once in every few
# hundred attempts, and falls back to passwords.
#
# If this bug is detected, PuTTY will pad its signatures in the way
# OpenSSH expects. If this bug is enabled when talking to a correct
# server, it is likely that no damage will be done, since correct
# servers usually still accept padded signatures because they're used
# to talking to OpenSSH.
#
# This is an SSH-2-specific bug.
#
#
# so i guess we just have to remove the leading 0, as i attempted this with a few and it doesn't work
# unless the leading 0 is removed (tried github.com and launchpad.net)
#
if modulus[0:1] == b'\x00':
# remove leading 0
return PuttyKnownhostEntry(keyName, hex(exponent), "0x" + binascii.hexlify(modulus[1:]).decode("utf-8"))
else:
return PuttyKnownhostEntry(keyName, hex(exponent), "0x" + binascii.hexlify(modulus).decode("utf-8"))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Program that converts between OpenSSH known hosts " +
"entries to PuTTY known hosts entries and vice versa", epilog="Copyright March 27, 2014 Mark Grandi")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--convert-to-putty", action="store_true")
group.add_argument("--convert-to-openssh", action="store_true")
parser.add_argument("--openssh-knownhosts-file", help="Specify a manual path to the openSSH " +
"known_hosts file, in case we can't find it automatically")
parser.add_argument("--should-resolve", action="store_true",
help="Whether or not to resolve ip addresses and store that rather then the 'text' hostname")
parser.add_argument("--output", nargs="?", type=argparse.FileType('w'), default=sys.stdout,
help="Where to save the output, either a .reg file if converting to putty or a known_hosts " +
" file if converting to openssh, default is stdout")
try:
main(parser.parse_args())
except Exception as e:
print("Something went wrong: {}".format(e))