diff --git a/.gitignore b/.gitignore index 88e1ce7..dccea14 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ build sshwot-export-known-hosts sshwot-filter +sshwot-verify diff --git a/Makefile b/Makefile index a893d3b..d43578c 100644 --- a/Makefile +++ b/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: diff --git a/src/default_files.py b/src/default_files.py index 7f0367d..4435e4a 100644 --- a/src/default_files.py +++ b/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 diff --git a/src/main-filter.py b/src/main-filter.py index 6b9850a..0c6bcca 100644 --- a/src/main-filter.py +++ b/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 diff --git a/src/main-verify.py b/src/main-verify.py new file mode 100644 index 0000000..f08a7e9 --- /dev/null +++ b/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)