mirror of
https://github.com/Horizontal42/netsleuth.git
synced 2026-11-10 22:42:03 +03:00
Warn users when TLS certificate verification is disabled
* Emit `RuntimeWarning` when `verify=False` is passed to `tls_context`. * Add explicit `pytest.warns` assertion to unit tests covering this bypass path. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
8c99ac78b7
commit
a0b5caeb5f
4 changed files with 45 additions and 14 deletions
|
|
@ -4,6 +4,7 @@ import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
|
import warnings
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from netsleuth.models import TlsResult
|
from netsleuth.models import TlsResult
|
||||||
|
|
@ -19,7 +20,9 @@ def split_timings(
|
||||||
return tcp_rtt_ms, handshake_ms, ttfb_ms
|
return tcp_rtt_ms, handshake_ms, ttfb_ms
|
||||||
|
|
||||||
|
|
||||||
def cpu_bound_ratio(tls_handshake_ms: float | None, tcp_rtt_ms: float | None) -> float | None:
|
def cpu_bound_ratio(
|
||||||
|
tls_handshake_ms: float | None, tcp_rtt_ms: float | None
|
||||||
|
) -> float | None:
|
||||||
if tls_handshake_ms is None or not tcp_rtt_ms:
|
if tls_handshake_ms is None or not tcp_rtt_ms:
|
||||||
return None
|
return None
|
||||||
return tls_handshake_ms / tcp_rtt_ms
|
return tls_handshake_ms / tcp_rtt_ms
|
||||||
|
|
@ -28,13 +31,20 @@ def cpu_bound_ratio(tls_handshake_ms: float | None, tcp_rtt_ms: float | None) ->
|
||||||
def tls_context(verify: bool = True) -> ssl.SSLContext:
|
def tls_context(verify: bool = True) -> ssl.SSLContext:
|
||||||
ctx = ssl.create_default_context()
|
ctx = ssl.create_default_context()
|
||||||
if not verify:
|
if not verify:
|
||||||
|
warnings.warn(
|
||||||
|
"TLS certificate verification is disabled. This connection is insecure.",
|
||||||
|
RuntimeWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
ctx.check_hostname = False
|
ctx.check_hostname = False
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
ctx.set_alpn_protocols(["h2", "http/1.1"])
|
ctx.set_alpn_protocols(["h2", "http/1.1"])
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
def fingerprint_verdict(host: str, actual_sha256: str | None, pins: dict[str, str]) -> str:
|
def fingerprint_verdict(
|
||||||
|
host: str, actual_sha256: str | None, pins: dict[str, str]
|
||||||
|
) -> str:
|
||||||
pin = pins.get(host)
|
pin = pins.get(host)
|
||||||
if not pin:
|
if not pin:
|
||||||
return "unpinned"
|
return "unpinned"
|
||||||
|
|
@ -76,7 +86,9 @@ async def measure_tls(
|
||||||
result = TlsResult(label=label, host=host, port=port)
|
result = TlsResult(label=label, host=host, port=port)
|
||||||
pins = pins or {}
|
pins = pins or {}
|
||||||
try:
|
try:
|
||||||
tcp_rtt_ms = await tcp_connect_rtt(host, port=port, timeout=timeout, source_ip=source_ip)
|
tcp_rtt_ms = await tcp_connect_rtt(
|
||||||
|
host, port=port, timeout=timeout, source_ip=source_ip
|
||||||
|
)
|
||||||
result.tcp_rtt_ms = tcp_rtt_ms
|
result.tcp_rtt_ms = tcp_rtt_ms
|
||||||
|
|
||||||
local_addr = (source_ip, 0) if source_ip else None
|
local_addr = (source_ip, 0) if source_ip else None
|
||||||
|
|
@ -84,7 +96,11 @@ async def measure_tls(
|
||||||
async def _connect(do_verify: bool):
|
async def _connect(do_verify: bool):
|
||||||
return await asyncio.wait_for(
|
return await asyncio.wait_for(
|
||||||
asyncio.open_connection(
|
asyncio.open_connection(
|
||||||
host, port, ssl=tls_context(verify=do_verify), server_hostname=host, local_addr=local_addr
|
host,
|
||||||
|
port,
|
||||||
|
ssl=tls_context(verify=do_verify),
|
||||||
|
server_hostname=host,
|
||||||
|
local_addr=local_addr,
|
||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
@ -124,7 +140,9 @@ async def measure_tls(
|
||||||
result.cert_not_after = parsed.get("notAfter")
|
result.cert_not_after = parsed.get("notAfter")
|
||||||
result.cert_days_remaining = days_remaining(result.cert_not_after)
|
result.cert_days_remaining = days_remaining(result.cert_not_after)
|
||||||
|
|
||||||
request = f"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n".encode()
|
request = (
|
||||||
|
f"HEAD / HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n".encode()
|
||||||
|
)
|
||||||
began_ttfb = time.perf_counter()
|
began_ttfb = time.perf_counter()
|
||||||
writer.write(request)
|
writer.write(request)
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
@ -137,7 +155,9 @@ async def measure_tls(
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_tcp_rtt_ms, handshake_ms, ttfb_ms = split_timings(tcp_rtt_ms, tls_total_ms, ttfb_ms)
|
_tcp_rtt_ms, handshake_ms, ttfb_ms = split_timings(
|
||||||
|
tcp_rtt_ms, tls_total_ms, ttfb_ms
|
||||||
|
)
|
||||||
result.tls_handshake_ms = handshake_ms
|
result.tls_handshake_ms = handshake_ms
|
||||||
result.ttfb_ms = ttfb_ms
|
result.ttfb_ms = ttfb_ms
|
||||||
except (OSError, TimeoutError, ssl.SSLError) as exc:
|
except (OSError, TimeoutError, ssl.SSLError) as exc:
|
||||||
|
|
@ -160,7 +180,15 @@ async def tls_fanout(
|
||||||
async def _bounded(label: str, host: str) -> TlsResult:
|
async def _bounded(label: str, host: str) -> TlsResult:
|
||||||
async with semaphore:
|
async with semaphore:
|
||||||
return await measure_tls(
|
return await measure_tls(
|
||||||
label, host, port=port, timeout=timeout, source_ip=source_ip, verify=verify, pins=pins
|
label,
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=timeout,
|
||||||
|
source_ip=source_ip,
|
||||||
|
verify=verify,
|
||||||
|
pins=pins,
|
||||||
)
|
)
|
||||||
|
|
||||||
return list(await asyncio.gather(*(_bounded(label, host) for label, host in targets)))
|
return list(
|
||||||
|
await asyncio.gather(*(_bounded(label, host) for label, host in targets))
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import os
|
from pathlib import PurePosixPath
|
||||||
from pathlib import Path, PurePosixPath
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from netsleuth.cli import _dedupe, _os_timezone, parse_target
|
from netsleuth.cli import _dedupe, _os_timezone, parse_target
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ def test_degradation_note_on_windows_mentions_the_api_rather_than_sysctl():
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from netsleuth.netinfo import iface_for_ip, is_tunnel_iface, mtu_anomaly, primary_interface_ip
|
from netsleuth.netinfo import iface_for_ip, is_tunnel_iface, mtu_anomaly, primary_interface_ip
|
||||||
|
|
||||||
|
|
@ -244,7 +243,6 @@ class MockStats:
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from netsleuth.netinfo import collect_local_net
|
from netsleuth.netinfo import collect_local_net
|
||||||
from netsleuth.models import LocalNet
|
|
||||||
|
|
||||||
@patch("netsleuth.netinfo.primary_interface_ip")
|
@patch("netsleuth.netinfo.primary_interface_ip")
|
||||||
@patch("netsleuth.netinfo.psutil.net_if_addrs")
|
@patch("netsleuth.netinfo.psutil.net_if_addrs")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import socket
|
||||||
import ssl
|
import ssl
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from netsleuth.probes.tls_rtt import (
|
from netsleuth.probes.tls_rtt import (
|
||||||
cert_name,
|
cert_name,
|
||||||
cpu_bound_ratio,
|
cpu_bound_ratio,
|
||||||
|
|
@ -63,6 +65,7 @@ def test_tls_context_verifies_by_default():
|
||||||
|
|
||||||
|
|
||||||
def test_tls_context_disables_verification_when_asked():
|
def test_tls_context_disables_verification_when_asked():
|
||||||
|
with pytest.warns(RuntimeWarning, match="TLS certificate verification is disabled"):
|
||||||
ctx = tls_context(verify=False)
|
ctx = tls_context(verify=False)
|
||||||
assert ctx.check_hostname is False
|
assert ctx.check_hostname is False
|
||||||
assert ctx.verify_mode == ssl.CERT_NONE
|
assert ctx.verify_mode == ssl.CERT_NONE
|
||||||
|
|
@ -83,7 +86,10 @@ def test_fingerprint_verdict_unpinned_when_host_has_no_pin():
|
||||||
|
|
||||||
|
|
||||||
def test_fingerprint_verdict_mismatch_when_actual_is_none():
|
def test_fingerprint_verdict_mismatch_when_actual_is_none():
|
||||||
assert fingerprint_verdict("example.com", None, {"example.com": "aabbccdd"}) == "mismatch"
|
assert (
|
||||||
|
fingerprint_verdict("example.com", None, {"example.com": "aabbccdd"})
|
||||||
|
== "mismatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_cert_name_flattens_the_rdn_sequence():
|
def test_cert_name_flattens_the_rdn_sequence():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue