Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
nervuri | 1bd6c34c11 | |
nervuri | c375807e26 | |
nervuri | 06a52879a8 | |
nervuri | f19ae805fc | |
nervuri | 1c8e8b661a | |
nervuri | 7f8c83c897 |
62
README.md
62
README.md
|
@ -1,40 +1,38 @@
|
|||
# gemini-demo-1
|
||||
# gemini-certificate-validation-demo-1
|
||||
|
||||
Minimal but usable interactive Gemini client in < 100 LOC of Python 3
|
||||
Minimal Gemini client capable of (self-signed) certificate validation
|
||||
using the additional network perspective of a Tor exit node.
|
||||
|
||||
## Rationale
|
||||
When the client encounters a new TLS certificate for a host, it connects
|
||||
to that same host via Tor, in order to obtain its certificate from a
|
||||
second vantage point. The user is notified on certificate mismatch or
|
||||
connection failure.
|
||||
|
||||
One of the original design criteria for the Gemini protocol was that
|
||||
"a basic but usable (not ultra-spartan) client should fit comfortably
|
||||
within 50 or so lines of code in a modern high-level language.
|
||||
Certainly not more than 100". This client was written to gauge how
|
||||
close to (or far from!) that goal the initial rough specification is.
|
||||
Any MITM attack (whether enabled by BGP hijack, DNS compromise or
|
||||
whatever else) will trigger an alert unless it affects both the user and
|
||||
the exit node at the same time. As such, this validation method works
|
||||
best when the exit node and the user are far apart and are not using the
|
||||
same DNS resolver.
|
||||
|
||||
## Capabilities
|
||||
Users may configure Tor to select specific exit nodes by setting
|
||||
the [ExitNodes](https://2019.www.torproject.org/docs/tor-manual.html.en#ExitNodes)
|
||||
and [StrictNodes](https://2019.www.torproject.org/docs/tor-manual.html.en#StrictNodes)
|
||||
options in their `torrc` file. The `ExitNodes` option accepts
|
||||
countries, IP address ranges and node fingerprints. For example, this
|
||||
is how to only select exits located in France:
|
||||
|
||||
This crude but functional client:
|
||||
```
|
||||
ExitNodes {fr}
|
||||
StrictNodes 1
|
||||
```
|
||||
|
||||
* Has a minimal interactive interface for "Gemini maps"
|
||||
* Will print plain text in any encoding if it is properly declared in
|
||||
the server's response header
|
||||
* Will handle binary files using programs specified in `/etc/mailcap`
|
||||
(so you can, e.g. view images)
|
||||
* Will follow redirects
|
||||
* Will report errors
|
||||
* Does NOT DO ANY validation of TLS certificates
|
||||
False alarms can be triggered by attacks on the exit node's end.
|
||||
And, obviously, validation does not work for servers which block Tor.
|
||||
|
||||
It's a *snug* fit in 100 lines, but it's possible. A 50 LOC client
|
||||
would need to be much simpler.
|
||||
Validated certificates are kept in memory for the duration of the
|
||||
browsing session. Tor is assumed to be listening on localhost, port
|
||||
9050.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the script and you'll get a prompt. Type a Gemini URL (the scheme
|
||||
is implied, so simply entering e.g. `gemini.conman.org` will work) to
|
||||
visit a Gemini location.
|
||||
|
||||
If a Gemini menu is visited, you'll see numeric indices for links, ala
|
||||
VF-1 or AV-98. Type a number to visit that link.
|
||||
|
||||
There is very crude history: you can type `b` to go "back".
|
||||
|
||||
Type `q` to quit.
|
||||
This is a fork of Solderpunk's [minimal Gemini
|
||||
client](https://tildegit.org/solderpunk/gemini-demo-1) written in
|
||||
Python.
|
||||
|
|
|
@ -8,6 +8,12 @@ import ssl
|
|||
import tempfile
|
||||
import textwrap
|
||||
import urllib.parse
|
||||
import socks
|
||||
|
||||
timeout = 5
|
||||
|
||||
tor_validation = True
|
||||
accepted_certs = {}
|
||||
|
||||
caps = mailcap.getcaps()
|
||||
menu = []
|
||||
|
@ -47,21 +53,82 @@ while True:
|
|||
# Do the Gemini transaction
|
||||
try:
|
||||
while True:
|
||||
s = socket.create_connection((parsed_url.netloc, 1965))
|
||||
context = ssl.SSLContext()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
s = context.wrap_socket(s, server_hostname = parsed_url.netloc)
|
||||
|
||||
host = parsed_url.hostname
|
||||
port = parsed_url.port or 1965
|
||||
|
||||
s = socket.create_connection((host, port), timeout)
|
||||
s = context.wrap_socket(s, server_hostname = host)
|
||||
cert = s.getpeercert(True)
|
||||
|
||||
if tor_validation \
|
||||
and (host not in accepted_certs \
|
||||
or accepted_certs[host] != cert):
|
||||
|
||||
# Verify TLS certificate from the vantage point
|
||||
# of a random Tor exit node.
|
||||
print("Validating certificate...")
|
||||
|
||||
# Get certificate via Tor (SOCKS5 @ localhost:9050).
|
||||
# DNS lookup is done over Tor.
|
||||
tor_socket = socks.socksocket()
|
||||
tor_socket.set_proxy(socks.SOCKS5, "127.0.0.1", 9050, rdns=True)
|
||||
tor_socket.settimeout(timeout)
|
||||
tor_connection_successful = True
|
||||
validation_problem_encountered = False
|
||||
try:
|
||||
tor_socket.connect((host, port))
|
||||
except socks.ProxyConnectionError:
|
||||
validation_problem_encountered = True
|
||||
tor_connection_successful = False
|
||||
print("Tor proxy not available on localhost:9050.")
|
||||
print("Certificate validation can't be performed.")
|
||||
print("Continue browsing with validation disabled? [y/N]")
|
||||
if input(">> ").strip().lower() == "y":
|
||||
tor_validation = False
|
||||
else:
|
||||
raise Exception("Connection cancelled.")
|
||||
except socks.GeneralProxyError:
|
||||
validation_problem_encountered = True
|
||||
tor_connection_successful = False
|
||||
print("Tor connection timed out.")
|
||||
print("Certificate validation can't be performed.")
|
||||
print("Continue connection? [y/N]")
|
||||
if input(">> ").strip().lower() != "y":
|
||||
raise Exception("Connection cancelled.")
|
||||
if tor_connection_successful:
|
||||
tor_socket = context.wrap_socket(tor_socket, server_hostname = host)
|
||||
cert_via_tor = tor_socket.getpeercert(True)
|
||||
tor_socket.shutdown(socket.SHUT_RDWR)
|
||||
tor_socket.close()
|
||||
# Compare certs.
|
||||
if cert != cert_via_tor:
|
||||
validation_problem_encountered = True
|
||||
print("[SECURITY WARNING] Certificate validation failed!")
|
||||
print("This MIGHT be a Man-in-the-Middle attack.")
|
||||
print("Continue connection? [y/N]")
|
||||
if input(">> ").strip().lower() != "y":
|
||||
raise Exception("Connection cancelled.")
|
||||
|
||||
accepted_certs[host] = cert
|
||||
|
||||
if not validation_problem_encountered:
|
||||
print("OK")
|
||||
print()
|
||||
|
||||
s.sendall((url + '\r\n').encode("UTF-8"))
|
||||
# Get header and check for redirects
|
||||
fp = s.makefile("rb")
|
||||
header = fp.readline()
|
||||
header = header.decode("UTF-8").strip()
|
||||
status, mime = header.split()
|
||||
status, mime = header.split(maxsplit=1)
|
||||
# Handle input requests
|
||||
if status.startswith("1"):
|
||||
# Prompt
|
||||
query = input("INPUT" + mime + "> ")
|
||||
query = input("INPUT " + mime + "> ")
|
||||
url += "?" + urllib.parse.quote(query) # Bit lazy...
|
||||
# Follow redirects
|
||||
elif status.startswith("3"):
|
||||
|
|
Loading…
Reference in New Issue