Browse Source

Add sshwot-verify

Juhani Krekelä 10 months ago
parent
commit
973ba3c63a
5 changed files with 141 additions and 6 deletions
  1. 1 0
      .gitignore
  2. 13 1
      Makefile
  3. 15 5
      src/default_files.py
  4. 5 0
      src/main-filter.py
  5. 107 0
      src/main-verify.py

+ 1 - 0
.gitignore

@@ -6,3 +6,4 @@ __pycache__
 build
 sshwot-export-known-hosts
 sshwot-filter
+sshwot-verify

+ 13 - 1
Makefile

@@ -1,4 +1,4 @@
-BINS:=sshwot-export-known-hosts sshwot-filter
+BINS:=sshwot-export-known-hosts sshwot-filter sshwot-verify
 
 SSHWOT_EXPORT_KNOWN_HOSTS_MAIN:=src/main-export-known-hosts.py
 SSHWOT_EXPORT_KNOWN_HOSTS_DEPS:=src/entry.py src/hashing.py src/process_known_hosts.py src/write_file.py
@@ -6,6 +6,9 @@ SSHWOT_EXPORT_KNOWN_HOSTS_DEPS:=src/entry.py src/hashing.py src/process_known_ho
 SSHWOT_FILTER_MAIN:=src/main-filter.py
 SSHWOT_FILTER_DEPS:=src/entry.py src/hashing.py src/default_files.py src/read_file.py src/write_file.py
 
+SSHWOT_VERIFY_MAIN:=src/main-verify.py
+SSHWOT_VERIFY_DEPS:=src/check_fingerprint.py src/default_files.py src/entry.py src/hashing.py src/read_file.py
+
 all: $(BINS)
 
 sshwot-export-known-hosts: $(SSHWOT_EXPORT_KNOWN_HOSTS_MAIN) $(SSHWOT_EXPORT_KNOWN_HOSTS_DEPS)
@@ -26,6 +29,15 @@ sshwot-filter: $(SSHWOT_FILTER_MAIN) $(SSHWOT_FILTER_DEPS)
 	cat build/$@.zip >> $@
 	chmod +x $@
 
+sshwot-verify: $(SSHWOT_VERIFY_MAIN) $(SSHWOT_VERIFY_DEPS)
+	mkdir -p build/$@
+	cp $(SSHWOT_VERIFY_DEPS) build/$@/
+	cp $(SSHWOT_VERIFY_MAIN) build/$@/__main__.py
+	zip --quiet --junk-paths build/$@.zip build/$@/*.py
+	echo '#!/usr/bin/env python3' > $@
+	cat build/$@.zip >> $@
+	chmod +x $@
+
 .PHONY: all clean distclean buildclean
 
 clean:

+ 15 - 5
src/default_files.py

@@ -1,8 +1,8 @@
 import os
 
-def open_all():
-	"""open_all() → [file(rb)]
-	Open the default sshwot files"""
+def list_all():
+	"""list_all() → [str]
+	List the default sshwot files"""
 
 	try:
 		homedir = os.environ['HOME']
@@ -17,10 +17,20 @@ def open_all():
 		return []
 
 	# Read all the .sshwot files from /.sshwot by default
-	files = []
+	paths = []
 	for dir_entry in sshwot_dir:
 		if dir_entry.split('.')[-1] == 'sshwot':
 			path = os.path.join(sshwot_dir_path, dir_entry)
-			files.append(open(path, 'rb'))
+			paths.append(path)
+
+	return paths
+
+def open_all():
+	"""open_all() → [file(rb)]
+	Open the default sshwot files"""
+
+	files = []
+	for path in list_all():
+		files.append(open(path, 'rb'))
 
 	return files

+ 5 - 0
src/main-filter.py

@@ -90,6 +90,11 @@ def main():
 			sys.exit(1)
 		# We encode this, because hashing.base64dec expects bytes
 		fingerprint = hashing.base64dec(args.fingerprint[7:].encode())
+		# A valid sha256 fingerprint is 32 bytes
+		if len(fingerprint) < 32:
+			raise Exception('Fingerprint too short')
+		elif len(fingerprint) > 32:
+			raise Exception('Fingerprint too long')
 	
 	else:
 		fingerprint = None

+ 107 - 0
src/main-verify.py

@@ -0,0 +1,107 @@
+import argparse
+import os
+import sys
+
+import check_fingerprint
+import default_files
+import entry
+import hashing
+import read_file
+
+def main():
+	# TODO: Do known_hosts files too
+	parser = argparse.ArgumentParser(
+		description = """Search sshwot files for matching fingerprints.""",
+		# We want to provide help on --help, but the default thing
+		# also adds -h, which we don't want
+		add_help = False
+	)
+
+	# --help to get help
+	parser.add_argument('--help',
+		action = 'help',
+		help = 'show this help message and exit'
+	)
+
+	# -p/--port for port, but host is a positional argument
+	parser.add_argument('-p', '--port',
+		action = 'store',
+		dest = 'port',
+		# Automatically convert to integer
+		type = int,
+		help = 'the port associated with the given host'
+	)
+
+	# Host and fingerprint are required
+	parser.add_argument('host',
+		help = 'the domain to check'
+	)
+	parser.add_argument('fingerprint',
+		help = 'the fingerprint to check'
+	)
+
+	# Input file(s)
+	# Don't use argparse.FileType('rb'), since we want to know the names
+	parser.add_argument('infiles',
+		nargs = '*',
+		# The text shown for these in the usage
+		metavar = 'sshwot-file',
+		help = 'a sshwot file to search'
+	)
+
+	# This automatically parses the command line args for us. If it
+	# returns, we have correct arguments
+	args = parser.parse_args()
+
+	# Default to port 22
+	port = 22 if args.port is None else args.port
+
+	# Check the validity of the fingerprint and de-base64 it
+	if args.fingerprint[0:7].upper() != 'SHA256:':
+		print('We can only handle sha256 fingerprints (starts with SHA256:)')
+		sys.exit(1)
+	# We encode this, because hashing.base64dec expects bytes
+	fingerprint = hashing.base64dec(args.fingerprint[7:].encode())
+	# A valid sha256 fingerprint is 32 bytes
+	if len(fingerprint) < 32:
+		raise Exception('Fingerprint too short')
+	elif len(fingerprint) > 32:
+		raise Exception('Fingerprint too long')
+	
+	# Use the default files if no input files were specified
+	if len(args.infiles) == 0:
+		infiles = default_files.list_all()
+	else:
+		infiles = args.infiles
+
+	# Check
+	for path in infiles:
+		# Remove the directory and the extension from the file
+		name = os.path.basename(path)
+		if name.split('.')[-1] == 'sshwot':
+			name = '.'.join(name.split('.')[:-1])
+
+		with open(path, 'rb') as f:
+			entries, file_comment = read_file.read(f)
+
+		success, fail, same_fingerprint = check_fingerprint.check(entries, args.host, port, fingerprint)
+
+		for match_host, match_port, match_comment in success:
+			# Use for display the same normalzed format as internally
+			# We do .decode() here, as it produces bytes
+			host_display = entry.normalize_host(match_host, match_port).decode()
+			print('[\x1b[32mok\x1b[0m] %s: %s: %s' % (name, host_display, match_comment))
+
+		for fail_host, fail_port, fail_comment in fail:
+			host_display = entry.normalize_host(fail_host, fail_port).decode()
+			print('[\x1b[31mfail\x1b[0m] %s: %s: %s' % (name, host_display, fail_comment))
+
+		for _, _, same_fingerprint_comment in same_fingerprint:
+			print('[same fingerprint] %s: (unknown host): %s' % (name, same_fingerprint_comment))
+
+if __name__ == '__main__':
+	try:
+		main()
+	except Exception as err:
+		print('Error: %s' % err, file=sys.stderr)
+		sys.exit(1)