Per-network, per-nick client certificates (EC P-256, self-signed, 10-year validity) stored as combined PEM files. Authentication cascade: SASL EXTERNAL > SASL PLAIN > NickServ IDENTIFY. New commands: GENCERT, CERTFP, DELCERT. GENCERT auto-registers the fingerprint with NickServ CERT ADD when the network is connected. Includes email verification module for NickServ registration and expanded NickServ interaction (IDENTIFY, REGISTER, VERIFY).
136 lines
4.5 KiB
Python
136 lines
4.5 KiB
Python
"""Tests for client certificate management."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bouncer.cert import (
|
|
cert_path,
|
|
delete_cert,
|
|
fingerprint,
|
|
generate_cert,
|
|
has_cert,
|
|
list_certs,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def data_dir(tmp_path: Path) -> Path:
|
|
"""Provide a temporary data directory."""
|
|
return tmp_path
|
|
|
|
|
|
class TestCertPath:
|
|
def test_standard_path(self, data_dir: Path) -> None:
|
|
p = cert_path(data_dir, "libera", "fabesune")
|
|
assert p == data_dir / "certs" / "libera" / "fabesune.pem"
|
|
|
|
|
|
class TestGenerateCert:
|
|
def test_creates_pem_file(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
assert pem.is_file()
|
|
assert pem == cert_path(data_dir, "libera", "testnick")
|
|
|
|
def test_pem_contains_cert_and_key(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
content = pem.read_text()
|
|
assert "BEGIN CERTIFICATE" in content
|
|
assert "BEGIN PRIVATE KEY" in content
|
|
|
|
def test_file_permissions(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
mode = pem.stat().st_mode & 0o777
|
|
assert mode == 0o600
|
|
|
|
def test_overwrites_existing(self, data_dir: Path) -> None:
|
|
pem1 = generate_cert(data_dir, "libera", "testnick")
|
|
fp1 = fingerprint(pem1)
|
|
pem2 = generate_cert(data_dir, "libera", "testnick")
|
|
fp2 = fingerprint(pem2)
|
|
assert pem1 == pem2
|
|
assert fp1 != fp2 # New cert = new fingerprint
|
|
|
|
|
|
class TestFingerprint:
|
|
def test_format(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
fp = fingerprint(pem)
|
|
parts = fp.split(":")
|
|
assert len(parts) == 32 # SHA-256 = 32 bytes
|
|
for part in parts:
|
|
assert len(part) == 2
|
|
int(part, 16) # Must be valid hex
|
|
|
|
def test_uppercase_hex(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
fp = fingerprint(pem)
|
|
assert fp == fp.upper()
|
|
|
|
def test_deterministic_for_same_cert(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
assert fingerprint(pem) == fingerprint(pem)
|
|
|
|
|
|
class TestHasCert:
|
|
def test_exists(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
assert has_cert(data_dir, "libera", "testnick") is True
|
|
|
|
def test_not_exists(self, data_dir: Path) -> None:
|
|
assert has_cert(data_dir, "libera", "testnick") is False
|
|
|
|
|
|
class TestDeleteCert:
|
|
def test_delete_existing(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
assert delete_cert(data_dir, "libera", "testnick") is True
|
|
assert has_cert(data_dir, "libera", "testnick") is False
|
|
|
|
def test_delete_nonexistent(self, data_dir: Path) -> None:
|
|
assert delete_cert(data_dir, "libera", "testnick") is False
|
|
|
|
def test_cleans_empty_dir(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
delete_cert(data_dir, "libera", "testnick")
|
|
assert not (data_dir / "certs" / "libera").exists()
|
|
|
|
|
|
class TestListCerts:
|
|
def test_empty(self, data_dir: Path) -> None:
|
|
assert list_certs(data_dir) == []
|
|
|
|
def test_list_all(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "nick1")
|
|
generate_cert(data_dir, "oftc", "nick2")
|
|
certs = list_certs(data_dir)
|
|
assert len(certs) == 2
|
|
networks = {c[0] for c in certs}
|
|
assert networks == {"libera", "oftc"}
|
|
|
|
def test_list_by_network(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "nick1")
|
|
generate_cert(data_dir, "oftc", "nick2")
|
|
certs = list_certs(data_dir, network="libera")
|
|
assert len(certs) == 1
|
|
assert certs[0][0] == "libera"
|
|
assert certs[0][1] == "nick1"
|
|
|
|
def test_list_multiple_per_network(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "nick1")
|
|
generate_cert(data_dir, "libera", "nick2")
|
|
certs = list_certs(data_dir, network="libera")
|
|
assert len(certs) == 2
|
|
nicks = {c[1] for c in certs}
|
|
assert nicks == {"nick1", "nick2"}
|
|
|
|
def test_fingerprints_present(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
certs = list_certs(data_dir)
|
|
assert len(certs) == 1
|
|
_, _, fp = certs[0]
|
|
assert ":" in fp
|
|
assert len(fp.split(":")) == 32
|