Critical: build_leaf_cert self-signed leaves with the leaf key instead of the YubiKey-held Intermediate CA key. Now extracts TBS, signs via pkcs11-tool (ECDSA-SHA384), reassembles, and verifies against the intermediate CA public key before returning. - setup-certauth.sh: ~1100 lines of stale inline api/ copies replaced with copy-from-repo (single source of truth); writes private /etc/certauth/certauth.env (0600); DB init loads env, no more swallowed errors; systemd unit gets EnvironmentFile= - config.py: YubiKey serials no longer hard-coded (env, fail closed); aarch64-only PKCS#11 path replaced with arch-neutral default; all paths env-overridable (CERTAUTH_*) - main.py: removed dead fastapi.security.CSRFProtection import (crashed startup); module-relative static/templates dirs; created_by resolved from the authenticated user instead of hard-coded 1; unclosed file handles fixed; domain_id 0 stored as NULL (FK bug) - models.py: certificates.domain_id FK pointed at users(id), now domains(id) - login: CSRF token now actually sent and validated - tests: 23 tests (auth, API flows, DER helpers, signing pipeline) - README, LICENSE, requirements.txt, pyproject.toml
116 lines
3.1 KiB
Python
116 lines
3.1 KiB
Python
"""API endpoint tests (no YubiKey required)."""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from main import app
|
|
|
|
ADMIN = {"username": "certauth", "password": "test-admin-pass-123"}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client():
|
|
with TestClient(app) as c:
|
|
yield c
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def auth(client):
|
|
r = client.post("/api/token", json=ADMIN)
|
|
assert r.status_code == 200
|
|
return {"Authorization": f"Bearer {r.json()['access_token']}"}
|
|
|
|
|
|
def test_health(client):
|
|
r = client.get("/api/health")
|
|
assert r.status_code == 200
|
|
assert r.json() == {"status": "ok"}
|
|
|
|
|
|
def test_token_wrong_password(client):
|
|
r = client.post("/api/token", json={"username": "certauth", "password": "wrong"})
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_token_unknown_user(client):
|
|
r = client.post("/api/token", json={"username": "nobody", "password": "x"})
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_me(client, auth):
|
|
r = client.get("/api/me", headers=auth)
|
|
assert r.status_code == 200
|
|
assert r.json() == {"username": "certauth"}
|
|
|
|
|
|
def test_me_without_token(client):
|
|
assert client.get("/api/me").status_code == 401
|
|
|
|
|
|
def test_login_page_renders_with_csrf(client):
|
|
r = client.get("/login")
|
|
assert r.status_code == 200
|
|
assert "csrf_token" in r.text
|
|
|
|
|
|
def test_dashboard_redirects_when_unauthenticated(client):
|
|
r = client.get("/", follow_redirects=False)
|
|
assert r.status_code == 302
|
|
assert r.headers["location"] == "/login"
|
|
|
|
|
|
def test_login_requires_valid_csrf(client):
|
|
r = client.post(
|
|
"/login",
|
|
data={"username": "certauth", "password": "test-admin-pass-123", "csrf_token": "bogus"},
|
|
follow_redirects=False,
|
|
)
|
|
assert r.status_code == 200
|
|
assert "Invalid request" in r.text
|
|
|
|
|
|
def test_domain_create_and_list(client, auth):
|
|
r = client.post(
|
|
"/api/domains",
|
|
data={"name": "test.example.ms", "description": "unit test domain"},
|
|
headers=auth,
|
|
)
|
|
assert r.status_code == 200
|
|
r = client.get("/api/domains", headers=auth)
|
|
assert r.status_code == 200
|
|
names = [d["name"] for d in r.json()]
|
|
assert "test.example.ms" in names
|
|
|
|
|
|
def test_domain_requires_auth(client):
|
|
r = client.post("/api/domains", data={"name": "x.ms"})
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_cert_request_lifecycle(client, auth):
|
|
client.post(
|
|
"/api/domains",
|
|
data={"name": "certtest.example.ms"},
|
|
headers=auth,
|
|
)
|
|
r = client.post(
|
|
"/api/certs/request",
|
|
data={"cn": "certtest.example.ms", "sans": "alt.example.ms"},
|
|
headers=auth,
|
|
)
|
|
assert r.status_code == 200
|
|
cert_id = r.json()["id"]
|
|
|
|
r = client.get("/api/certs", headers=auth)
|
|
row = next(c for c in r.json() if c["id"] == cert_id)
|
|
assert row["status"] == "pending"
|
|
assert row["subject"] == "certtest.example.ms"
|
|
|
|
# Signing requires a physical YubiKey; must fail cleanly, not 500-crash.
|
|
r = client.post(f"/api/certs/{cert_id}/sign", headers=auth)
|
|
assert r.status_code == 500
|
|
|
|
|
|
def test_sign_missing_cert(client, auth):
|
|
assert client.post("/api/certs/99999/sign", headers=auth).status_code == 400
|