"""A pinned certificate is trusted for what it is, not for its dates.""" import datetime import socket import ssl import threading from collections.abc import Iterator from pathlib import Path import pytest from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID from fluksio.flow.connector_tls import ( CertificateChanged, context_for, pin_key, pinned_certificate, raise_if_changed, ) def _expired(name: str) -> tuple[ec.EllipticCurvePrivateKey, str]: """A self-signed certificate that expired a month ago.""" key = ec.generate_private_key(ec.SECP256R1()) now = datetime.datetime.now(datetime.UTC) subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]) certificate = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(subject) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(now - datetime.timedelta(days=60)) .not_valid_after(now - datetime.timedelta(days=30)) .sign(key, hashes.SHA256()) ) return key, certificate.public_bytes(serialization.Encoding.PEM).decode() DEVICE = _expired("00005e005301") IMPOSTOR = _expired("00005e005302") class Store: def __init__(self) -> None: self.values: dict[str, str] = {} def get(self, key: str) -> str | None: return self.values.get(key) def set(self, key: str, value: str) -> None: self.values[key] = value @pytest.fixture def device(tmp_path: Path) -> Iterator[str]: """A TLS server presenting the expired certificate; yields its address.""" key, pem = DEVICE (tmp_path / "cert.pem").write_text(pem) (tmp_path / "key.pem").write_bytes( key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption(), ) ) context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(tmp_path / "cert.pem", tmp_path / "key.pem") listener = socket.create_server(("127.0.0.1", 0)) def serve() -> None: while True: try: connection, _ = listener.accept() except OSError: return try: context.wrap_socket(connection, server_side=True).close() except OSError: connection.close() threading.Thread(target=serve, daemon=True).start() yield f"127.0.0.1:{listener.getsockname()[1]}" listener.close() def _handshake(address: str, pem: str) -> None: host, port = address.split(":") with socket.create_connection((host, int(port)), timeout=5) as raw: context_for(pem).wrap_socket(raw, server_hostname=host).close() def test_an_expired_pinned_certificate_is_still_trusted(device: str) -> None: store = Store() pem = pinned_certificate(store, device, 5.0) assert store.get(pin_key(device)) == DEVICE[1] _handshake(device, pem) # raises if the dates were checked def test_another_certificate_is_still_refused(device: str) -> None: pinned = IMPOSTOR[1] with pytest.raises(ssl.SSLCertVerificationError) as refused: _handshake(device, pinned) with pytest.raises(CertificateChanged, match="/connectors/test/state/cert:"): raise_if_changed(refused.value, pinned, device, 5.0, "test")