Initial implementation of TOFU security model.

This commit is contained in:
Solderpunk 2020-05-16 18:58:53 +02:00
parent cbd1ff48e9
commit d1412377da
1 changed files with 73 additions and 1 deletions

74
av98.py
View File

@ -13,8 +13,10 @@ import cmd
import cgi import cgi
import codecs import codecs
import collections import collections
import datetime
import fnmatch import fnmatch
import glob import glob
import hashlib
import io import io
import mimetypes import mimetypes
import os import os
@ -23,6 +25,7 @@ import random
import shlex import shlex
import shutil import shutil
import socket import socket
import sqlite3
import ssl import ssl
import subprocess import subprocess
import sys import sys
@ -270,6 +273,18 @@ class GeminiClient(cmd.Cmd):
"timeouts": 0, "timeouts": 0,
} }
self._connect_to_tofu_db()
def _connect_to_tofu_db(self):
db_path = os.path.join(self.config_dir, "tofu.db")
self.db_conn = sqlite3.connect(db_path)
self.db_cur = self.db_conn.cursor()
self.db_cur.execute("""CREATE TABLE IF NOT EXISTS cert_cache
(hostname text, address text, fingerprint text,
first_seen date, last_seen date, count integer)""")
def _go_to_gi(self, gi, update_hist=True, handle=True): def _go_to_gi(self, gi, update_hist=True, handle=True):
"""This method might be considered "the heart of AV-98". """This method might be considered "the heart of AV-98".
Everything involved in fetching a gemini resource happens here: Everything involved in fetching a gemini resource happens here:
@ -595,6 +610,10 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
self._debug("Established {} connection.".format(s.version())) self._debug("Established {} connection.".format(s.version()))
self._debug("Cipher is: {}.".format(s.cipher())) self._debug("Cipher is: {}.".format(s.cipher()))
# Do TOFU
cert = s.getpeercert(binary_form=True)
self._validate_cert(address[4][0], host, cert)
# Remember that we showed the current cert to this domain... # Remember that we showed the current cert to this domain...
if self.client_certs["active"]: if self.client_certs["active"]:
self.active_cert_domains.append(gi.host) self.active_cert_domains.append(gi.host)
@ -624,6 +643,57 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
return addresses return addresses
def _validate_cert(self, address, host, cert):
sha = hashlib.sha256()
sha.update(cert)
fingerprint = sha.hexdigest()
now = datetime.datetime.now()
# Have we been here before?
self.db_cur.execute("""SELECT fingerprint, first_seen, last_seen, count
FROM cert_cache
WHERE hostname=? AND address=?""", (host, address))
cached_certs = self.db_cur.fetchall()
# If so, check for a match
if cached_certs:
max_count = 0
for cached_fingerprint, first, last, count in cached_certs:
if count > max_count:
max_count = count
if fingerprint == cached_fingerprint:
# Matched!
self._debug("TOFU: Accepting previously seen ({} times) certificate {}".format(count, fingerprint))
self.db_cur.execute("""UPDATE cert_cache
SET last_seen=?, count=?
WHERE hostname=? AND address=? AND fingerprint=?""",
(now, count+1, host, address, fingerprint))
break
else:
self._debug("TOFU: Unrecognised certificate {}! Raising the alarm...".format(fingerprint))
print("****************************************")
print("[SECURITY WARNING] Unrecognised certificate!")
print("The certificate presented for {} ({}) has never been seen before.".format(host, address))
print("A different certificate has previously been seen {} times.".format(max_count))
print("This MIGHT be a Man-in-the-Middle attack.")
print("****************************************")
print("Attempt to verify the new certificate fingerprint out-of-band:")
print(fingerprint)
choice = input("Accept this new certificate? Y/N ").strip().lower()
if choice in ("y", "yes"):
self.db_cur.execute("""INSERT INTO cert_cache
VALUES (?, ?, ?, ?, ?, ?)""",
(host, address, fingerprint, now, now, 1))
else:
raise Exception("TOFU Failure!")
# If not, cache this cert
else:
self._debug("TOFU: Blindly trusting first ever certificate for this host!")
self.db_cur.execute("""INSERT INTO cert_cache
VALUES (?, ?, ?, ?, ?, ?)""",
(host, address, fingerprint, now, now, 1))
def _get_handler_cmd(self, mimetype): def _get_handler_cmd(self, mimetype):
# Now look for a handler for this mimetype # Now look for a handler for this mimetype
# Consider exact matches before wildcard matches # Consider exact matches before wildcard matches
@ -1272,6 +1342,9 @@ current gemini browsing session."""
### The end! ### The end!
def do_quit(self, *args): def do_quit(self, *args):
"""Exit AV-98.""" """Exit AV-98."""
# Close TOFU DB
self.db_conn.commit()
self.db_conn.close()
# Clean up after ourself # Clean up after ourself
if self.tmp_filename: if self.tmp_filename:
os.unlink(self.tmp_filename) os.unlink(self.tmp_filename)
@ -1282,7 +1355,6 @@ current gemini browsing session."""
certfile = os.path.join(self.config_dir, "transient_certs", cert+ext) certfile = os.path.join(self.config_dir, "transient_certs", cert+ext)
if os.path.exists(certfile): if os.path.exists(certfile):
os.remove(certfile) os.remove(certfile)
print() print()
print("Thank you for flying AV-98!") print("Thank you for flying AV-98!")
sys.exit() sys.exit()