Fixed a couple of diagnostics checks

This commit is contained in:
Michael Lazar 2020-01-07 15:30:36 -05:00
parent cbbedb5f1d
commit e43587493e
1 changed files with 55 additions and 18 deletions

View File

@ -11,6 +11,7 @@ should be taken with a grain of salt and analyzed on their own merit.
import argparse import argparse
import contextlib import contextlib
import datetime import datetime
import ipaddress
import socket import socket
import ssl import ssl
import sys import sys
@ -122,7 +123,8 @@ class BaseCheck:
proto = socket.IPPROTO_TCP proto = socket.IPPROTO_TCP
addr_info = socket.getaddrinfo(host, port, family, type_, proto) addr_info = socket.getaddrinfo(host, port, family, type_, proto)
if not addr_info: if not addr_info:
raise UserWarning("No address found for host") raise UserWarning(f"No {family} address found for host")
# Gemini IPv6
return addr_info[0][4] return addr_info[0][4]
@contextlib.contextmanager @contextlib.contextmanager
@ -193,8 +195,10 @@ class IPv6Address(BaseCheck):
def check(self) -> None: def check(self) -> None:
log(f"Looking up IPv6 address for {self.args.host!r}") log(f"Looking up IPv6 address for {self.args.host!r}")
addr = self.resolve_host(socket.AF_INET6) addr = self.resolve_host(socket.AF_INET6)
if addr[0].startswith("::ffff:"): if ipaddress.ip_address(addr[0]).ipv4_mapped:
raise UserWarning("Found an IPv4-mapped address, skipping check") raise UserWarning(
"Found IPv4-mapped address, is your network IPv6 enabled?"
)
log(f"{addr[0]!r}", style="success") log(f"{addr[0]!r}", style="success")
log(f"Attempting to connect to [{addr[0]}]:{addr[1]}") log(f"Attempting to connect to [{addr[0]}]:{addr[1]}")
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock:
@ -225,12 +229,17 @@ class TLSClaims(BaseCheck):
def check(self) -> None: def check(self) -> None:
try: try:
# $ pip install cryptography
import cryptography
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID, ExtensionOID
except ImportError: except ImportError:
raise UserWarning("Could not import cryptography library, skipping") raise UserWarning("cryptography library not installed, skipping check")
with self.connection() as sock: with self.connection() as sock:
# Python refuses to parse a certificate unless the issuer is validated.
# Because many gemini servers use self-signed certs, we need to use
# a third-party library to parse the certs from their binary form.
der_x509 = sock.getpeercert(binary_form=True) der_x509 = sock.getpeercert(binary_form=True)
cert = default_backend().load_der_x509_certificate(der_x509) cert = default_backend().load_der_x509_certificate(der_x509)
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
@ -243,11 +252,33 @@ class TLSClaims(BaseCheck):
style = "success" if cert.not_valid_after >= now else "failure" style = "success" if cert.not_valid_after >= now else "failure"
log(f"{cert.not_valid_after} UTC", style) log(f"{cert.not_valid_after} UTC", style)
log("Checking common name matches hostname") log("Checking subject claim matches server hostname")
cn = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value subject = []
cert_dict = {"subject": ((("commonName", cn),),)} for cn in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
subject.append(("commonName", cn.value))
subject_alt_name = []
try:
ext = cert.extensions.get_extension_for_oid(
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
except cryptography.x509.ExtensionNotFound:
pass
else:
for dns in ext.value.get_values_for_type(cryptography.x509.DNSName):
subject_alt_name.append(("DNS", dns))
for ip_address in ext.value.get_values_for_type(
cryptography.x509.IPAddress
):
subject_alt_name.append(("IP Address", ip_address))
cert_dict = {
"subject": (tuple(subject),),
"subjectAltName": tuple(subject_alt_name),
}
log(f"{cert_dict!r}", style="info")
ssl.match_hostname(cert_dict, self.args.host) ssl.match_hostname(cert_dict, self.args.host)
log(repr(cn), style="success") log(f"Hostname {self.args.host!r} matches claim", style="success")
class TLSVerified(BaseCheck): class TLSVerified(BaseCheck):
@ -274,6 +305,7 @@ class TLSRequired(BaseCheck):
def check(self) -> None: def check(self) -> None:
log("Sending non-TLS request") log("Sending non-TLS request")
try:
with socket.create_connection((self.args.host, self.args.port)) as sock: with socket.create_connection((self.args.host, self.args.port)) as sock:
sock.sendall(f"gemini://{self.netloc}\r\n".encode()) sock.sendall(f"gemini://{self.netloc}\r\n".encode())
fp = sock.makefile("rb") fp = sock.makefile("rb")
@ -282,6 +314,9 @@ class TLSRequired(BaseCheck):
log(f"Received unexpected response {header!r}", style="failure") log(f"Received unexpected response {header!r}", style="failure")
else: else:
log(f"Connection closed by server", style="success") log(f"Connection closed by server", style="success")
except Exception as e:
# A connection error is a valid response
log(f"{e}", style="success")
class ConcurrentConnections(BaseCheck): class ConcurrentConnections(BaseCheck):
@ -547,7 +582,7 @@ class URLSchemeMissing(BaseCheck):
"""A URL without a scheme should be inferred as gemini""" """A URL without a scheme should be inferred as gemini"""
def check(self) -> None: def check(self) -> None:
url = f"://{self.netloc}/\r\n" url = f"//{self.netloc}/\r\n"
response = self.make_request(url) response = self.make_request(url)
log("Status should be 20 (SUCCESS)") log("Status should be 20 (SUCCESS)")
@ -591,6 +626,8 @@ class URLDotEscape(BaseCheck):
log(f"{response.status!r}", style) log(f"{response.status!r}", style)
# TODO: Test sending a transient client certificate
# TODO: Test with client pinned to TLS v1.1
CHECKS = [ CHECKS = [
IPv4Address, IPv4Address,
IPv6Address, IPv6Address,