Compare commits

...

6 Commits

Author SHA1 Message Date
nervuri 1bd6c34c11 Clarify README. 2022-02-17 20:20:22 +02:00
nervuri c375807e26 Update README. 2022-02-12 20:00:47 +02:00
nervuri 06a52879a8 Add certificate validation capability. 2022-02-09 19:53:39 +02:00
nervuri f19ae805fc Add space after INPUT. 2022-02-09 19:46:09 +02:00
nervuri 1c8e8b661a Allow for blanks in <META> portion of response.
Credit: Simon Forman

d610a304e0
2022-02-09 19:42:48 +02:00
nervuri 7f8c83c897 Make gemini-demo.py executable. 2022-02-09 19:40:57 +02:00
2 changed files with 101 additions and 36 deletions

View File

@ -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.

75
gemini-demo.py Normal file → Executable file
View File

@ -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"):