Fixed a couple of diagnostics checks
This commit is contained in:
parent
cbbedb5f1d
commit
e43587493e
|
@ -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,14 +305,18 @@ class TLSRequired(BaseCheck):
|
||||||
|
|
||||||
def check(self) -> None:
|
def check(self) -> None:
|
||||||
log("Sending non-TLS request")
|
log("Sending non-TLS request")
|
||||||
with socket.create_connection((self.args.host, self.args.port)) as sock:
|
try:
|
||||||
sock.sendall(f"gemini://{self.netloc}\r\n".encode())
|
with socket.create_connection((self.args.host, self.args.port)) as sock:
|
||||||
fp = sock.makefile("rb")
|
sock.sendall(f"gemini://{self.netloc}\r\n".encode())
|
||||||
header = fp.readline().decode()
|
fp = sock.makefile("rb")
|
||||||
if header:
|
header = fp.readline().decode()
|
||||||
log(f"Received unexpected response {header!r}", style="failure")
|
if header:
|
||||||
else:
|
log(f"Received unexpected response {header!r}", style="failure")
|
||||||
log(f"Connection closed by server", style="success")
|
else:
|
||||||
|
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,
|
||||||
|
|
Loading…
Reference in New Issue