88
81
hostname = hostname + "."
89
82
return hostname + " IN SSHFP " + keytype + " 1 " + fpsha1
91
def sshfpFromFile(khfile,wantedHosts):
84
def get_known_host_entry(known_hosts, host):
85
"""Get a single entry out of a known_hosts file
87
Uses the ssh-keygen utility."""
88
cmd = ["ssh-keygen", "-f", known_hosts, "-F", host]
89
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
90
(stdout, stderr) = process.communicate()
91
if not quiet and stderr:
92
print >>sys.stderr, stderr
94
for e in stdout.split("\n"):
95
if not e.startswith("#"):
97
return "\n".join(outputl)
99
def sshfp_from_file(khfile,wantedHosts):
93
104
known_hosts = os.path.expanduser(khfile)
95
106
khfp = open(known_hosts)
97
print "Failed to open file "+ known_hosts
108
print >>sys.stderr, "ERROR: failed to open file "+ known_hosts
110
hashed_known_hosts = False
111
if khfp.readline().startswith("|1|"):
112
hashed_known_hosts = True
101
return processRaw(entries,wantedHosts)
103
def processRaw(entries,wantedHosts):
107
for line in entries.split("\n"):
108
if line != "" and line[0] != "#" and line[0] != "|" and line !="\n":
109
records = line.split(" ")
110
hosts = records[0].split(",")
111
for hostname in hosts:
112
if all_hosts or (hostname in wantedHosts) or (hostname == wantedHosts):
115
# note that ssh-keyscan and known_hosts don't use the same string
116
# for all algos, eg ssh-dss vs -t dsa, match on all but last char
117
if (algo != "dsa,rsa") and keytype[:-1] != "ssh-%s"%algo[:-1]:
119
key64blob = records[2]
120
record = create_sshfp(hostname,keytype,key64blob)
122
allrecords.append(record)
127
# join records, dnssigner wants a newline at end of file, so add one.
128
return "\n".join(allrecords)+"\n"
132
def getRecord(domain,type):
116
if hashed_known_hosts and all_hosts:
117
print >>sys.stderr, "ERROR: %s is hashed, cannot use with -a" % known_hosts
119
elif hashed_known_hosts: #only looking for some known hosts
120
for host in wantedHosts:
121
fingerprints.append(process_record(get_known_host_entry(known_hosts, host), host))
124
khfp = open(known_hosts)
126
print >>sys.stderr, "ERROR: failed to open file "+ known_hosts
130
fingerprints.append(process_records(data, wantedHosts))
131
return "\n".join(fingerprints)
133
def check_keytype(keytype):
136
if "ssh-%s"%algo[:-1] == keytype[:-1]:
139
print >>sys.stderr, "Could only find key type %s for %s" % (keytype, hostname)
142
def process_record(record, hostname):
143
(host, keytype, key) = record.split(" ")
145
if check_keytype(keytype):
146
record = create_sshfp(hostname, keytype, key)
150
def process_records(data, hostnames):
151
"""Process all records in a string.
153
If the global "all_hosts" is True, then return SSHFP entries
154
for all records with the allowed key types.
156
If "all_hosts is False and hostnames is non-empty, return
157
only the items in hostnames
160
for record in data.split("\n"):
161
if record.startswith("#") or not record:
164
(host, keytype, key) = record.split(" ")
167
print >>sys.stdout, "Print unable to read record '%s'" % record
170
host = host.split(",")[0]
171
if all_hosts or host in hostnames or host == hostnames:
172
if not check_keytype(keytype):
174
all_records.append(create_sshfp(host, keytype, key))
177
return "\n".join(all_records)
181
def get_record(domain, qtype):
134
answers = dns.resolver.query(domain, type)
183
answers = dns.resolver.query(domain, qtype)
135
184
except dns.resolver.NXDOMAIN:
136
185
#print "NXdomain: "+domain
141
190
for rdata in answers:
142
191
# just return first entry we got, answers[0].target does not work
146
195
return str(rdata.target)
148
print "error in getRecord, unknown type "+type
197
print >>sys.stderr, "ERROR: error in get_record, unknown type " + qtype
151
def getAXFRrecord(domain,ns):
200
def get_axfr_record(domain, nameserver):
153
zone = dns.zone.from_xfr(dns.query.xfr(ns,domain))
202
zone = dns.zone.from_xfr(dns.query.xfr(nameserver, domain, rdtype=dns.rdatatype.AXFR))
154
203
except dns.exception.FormError:
155
204
raise dns.exception.FormError, domain
159
def sshfpFromAXFR(domain,nameserver):
160
if domain[-1] == " ":
208
def sshfp_from_axfr(domain, nameserver):
162
209
if " " in domain:
163
print "error: space in domain '"+domain+"' can't be right, aborted"
210
print >>sys.stderr, "ERROR: space in domain '"+domain+"' can't be right, aborted"
165
212
if not nameserver:
166
nameserver = getRecord(domain,"NS")
213
nameserver = get_record(domain, "NS")
167
214
if not nameserver:
168
print "warning: no NS record found for domain "+domain+". trying as host record instead"
215
print >>sys.stderr, "WARNING: no NS record found for domain "+domain+". trying as host record instead"
169
216
# better then nothing
170
return sshfpFromDNS(domain)
217
return sshfp_from_dns([domain])
172
219
#print "nameserver:" + str(ns)
174
221
# print "trying axfr for "+domain+"@"+nameserver
175
axfr = getAXFRrecord(domain,nameserver)
222
axfr = get_axfr_record(domain,nameserver)
176
223
except dns.exception.FormError, badDomain:
177
print "AXFR error: " + nameserver + " - No permission or not authorative for " + badDomain + "; aborting"
224
print >>sys.stderr, "AXFR error: %s - No permission or not authorative for %s; aborting" % (nameserver, badDomain)
180
227
for (name, ttl, rdata) in axfr.iterate_rdatas('A'):
181
228
#print "name:" +str(name) +", ttl:"+ str(ttl)+ ", rdata:"+str(rdata)
182
229
if "@" in str(name):
183
hosts = hosts + " " + domain + "."
230
hosts.append(domain + ".")
185
232
if not str(name) == "localhost":
186
hosts = hosts + " " + str(name) + "." + domain + "."
187
return sshfpFromDNS(hosts)
233
hosts.append("%s.%s." % (name, domain))
234
return sshfp_from_dns(hosts)
189
def sshfpFromDNS(hosts):
236
def sshfp_from_dns(hosts):
196
cmd = "ssh-keyscan -p %s -T %s -t %s %s" % (port, timeout, algo, hosts)
198
cmd = cmd + " 2>/dev/null"
201
p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE,
202
stderr=PIPE, close_fds=True)
203
tochild, fromchild, childerror = (p.stdin, p.stdout, p.stderr)
205
tochild, fromchild, childerror = os.popen3(cmd, 'r')
207
err = childerror.readlines()
208
khdns = "\n".join(fromchild.readlines())
211
print >>sys.stderr, e
212
return processRaw(khdns,hosts)
241
cmd = ["ssh-keyscan", "-p", str(port), "-T", str(timeout), "-t", ",".join(algos)] + hosts
242
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
243
(stdout, stderr) = process.communicate()
245
print >>sys.stderr, stderr
247
return process_records(khdns,hosts)
226
opts, args = getopt.getopt(argv[1:], "qhdvsT:t:a:o:k:p:", ["quiet","help","trailing-dot","version","scan","timeout:","type:","all:","output:","knownhosts:", "port:"])
227
except getopt.error, msg:
228
#print >>sys.stderr, err.msg
229
print >>sys.stderr, "ERROR parsing options"
257
parser = optparse.OptionParser()
258
parser.add_option("-k", "--knownhosts", "--known-hosts",
261
metavar="KNOWN_HOSTS_FILE",
263
help="obtain public ssh keys from the known_hosts file KNOWN_HOSTS_FILE")
264
parser.add_option("-s", "--scan",
268
help="scan the listed hosts for public keys using ssh-keyscan")
269
parser.add_option("-a", "--all",
272
help="scan all hosts in the known_hosts file when used with -k. When used with -s, attempt a zone transfer to obtain all A records in the domain provided")
273
parser.add_option("-d", "--trailing-dot",
276
help="add a trailing dot to the hostname in the SSHFP records")
277
parser.add_option("-o", "--output",
281
help="write to FILENAME instead of stdout")
282
parser.add_option("-p", "--port",
287
help="use PORT for scanning")
288
parser.add_option("-v", "--version",
291
help="print version information and exit")
292
parser.add_option("-q", "--quiet",
295
parser.add_option("-T", "--timeout",
300
help="scanning timeout (default %default)")
301
parser.add_option("-t", "--type",
305
choices=["rsa", "dsa"],
307
help="key type to fetch (may be specified more than once, default dsa,rsa)")
308
parser.add_option("-n", "--nameserver",
313
help="nameserver to use for AXFR (only valid with -s -a)")
314
(options, args) = parser.parse_args()
317
khfile = options.known_hosts
321
output = options.output
322
quiet = options.quiet
324
trailing = options.trailing_dot
325
timeout = options.timeout
326
algos = options.algo or ["dsa", "rsa"]
327
all_hosts = options.all_hosts
249
#if not opts and not args:
254
if o in ("-v", "--version"):
255
print "sshfp version: "+version
256
print "Authors:\n Paul Wouters <paul@xelerance.com>\n Jake Appelbaum <jacob@appelbaum.net>"
257
print "Source : http://www.xelerance.com/software/sshfp/"
259
if o in ("-h", "--help"):
262
if o in ("-d", "--trailling-dot"):
264
if o in ("-T", "--timeout"):
266
print "error: no timeout specified"
269
timeout = str(int(a))
271
print "error: timeout not specified in seconds"
273
if o in ("-t", "--type"):
275
print "error: no type specified"
277
if (a == "rsa") or (a == "dsa"):
280
print "error: invalid type"
282
if o in ("-q", "--quiet"):
284
if o in ("-p", "--port"):
288
if not quiet and port <> 22:
289
print "WARNING: non-standard port numbers are not designated in SSHFP records"
291
print "error: port must be a number"
293
if o in ("-a", "--all"):
297
if o in ("-o", "--output"):
299
print "error: no output file specified"
303
if o in ("-k", "--knownhosts"):
305
# optional arguments dont work cleanly in python??
307
khfile = "~/.ssh/known_hosts"
309
if os.path.isfile(a):
313
arec = getRecord(a,"A")
315
# it's really a hostname argument, not a known_hosts file.
317
khfile = "~/.ssh/known_hosts"
319
# no file and no domain, prob an arg mistaken as option
321
khfile = "~/.ssh/known_hosts"
323
# I guess we can't append opts for processing within
324
# the loop. Guess we need to exec a new sshfp or refactor.
325
# catch most commonly used options, eg "sshfp -k -a"
331
print "error: "+a+" is neither a known_hosts file or hostname"
334
if o in ("-s", "--scan"):
336
# add any args to -s as arguments
337
# currently not possible in getopts call
341
# print "DEBUG: opts"
343
# print "DEBUG: args"
346
if (not dodns and not dofile):
334
if not quiet and port <> 22:
335
print >>sys.stderr, "WARNING: non-standard port numbers are not designated in SSHFP records"
336
if not quiet and options.known_hosts and options.scan:
337
print >>sys.stderr, "WARNING: Ignoring -k option, -s was passwd"
338
if options.nameserver and not options.scan and not options.all_hosts:
339
print >>sys.stderr, "ERROR: Cannot specify -n without -s and -a"
341
if not options.scan and options.all_hosts and args:
342
print >>sys.stderr, "WARNING: -a and hosts both passed, ignoring manual host list"
344
print >>sys.stderr, "WARNING: Assuming -a"
347
khfile = DEFAULT_KNOWN_HOSTS_FILE
349
if options.scan and options.all_hosts:
352
datal.append(sshfp_from_axfr(arg, options.nameserver))
355
datal.insert(0, "; Generated by sshfp %s from %s at %s" % (VERSION, nameserver, time.ctime()))
357
data = "\n".join(datal)
358
elif options.scan: # Scan specified hosts
353
khfile = "~/.ssh/known_hosts"
357
if (dodns and dofile):
358
print "use either -k or -s"
364
# filter special case for using @nameserver, verify for misinterpreted options as args
369
#print "found ns:"+arg[1:]
372
print "WARNING: ssh-keyscan does not support @nameserver syntax, ignoring"
374
newargs = newargs + arg +" "
376
# shit, misinterpreted option as argument. We'll try to be more clever in the future.
380
print "error: No hostnames specified"
383
data = sshfpFromAXFR(newargs,nameserver)
385
data = ";\n; Generated by sshfp "+ version +" from " + nameserver + " at "+ time.ctime() +"\n;\n" + data
387
data = sshfpFromDNS(newargs)
390
data = sshfpFromFile(khfile,args)
360
print >>sys.stderr, "ERROR: You asked me to scan, but didn't give any hosts to scan"
362
data = sshfp_from_dns(args)
364
data = sshfp_from_file(khfile, args)
397
fp = open(output,"w")
368
fp = open(output, "w")
399
print "error: can't open '"+output+"' for writing"
370
print >>sys.stderr, "ERROR: can't open '%s'' for writing" % output
407
378
if __name__ == "__main__":