Fix OCSP publication races for revocation changes

This commit is contained in:
2026-10-25 11:19:17 +00:00
parent 44a79f5b0c
commit 404facf8e2
4 changed files with 157 additions and 10 deletions
+131
View File
@@ -456,6 +456,137 @@ class OcspSnapshotTests(unittest.TestCase):
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is True
@async_test
async def test_existing_revocation_marks_snapshot_dirty(self) -> None:
from dataclasses import replace
from unittest.mock import patch
from kca import ocsp
from kca.models import OcspSnapshot
async with api_client() as client:
with oidc_user("alice"):
vault_id = await _vault(client)
ca_key = await _key(client, vault_id, "existing dirty ca")
ca = await _cert(client, vault_id, ca_key["id"], "Existing Dirty CA", ca=True)
leaf_key = await _key(client, vault_id, "existing dirty leaf")
leaf = await _cert(
client, vault_id, leaf_key["id"], "Existing Dirty Leaf", signer_id=ca["id"]
)
configured = replace(ocsp.settings, ocsp_s3_bucket="test-bucket")
with patch.object(ocsp, "settings", configured):
response = await client.post(
f"/api/vaults/{vault_id}/certificates/{leaf['id']}/revoke",
json={"reason": "superseded"},
)
assert response.status_code == 200, response.text
await OcspSnapshot.filter(ca_id=ca["id"]).update(dirty=False)
response = await client.post(
f"/api/vaults/{vault_id}/certificates/{leaf['id']}/revoke",
json={"reason": "superseded"},
)
assert response.status_code == 200, response.text
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is True
@async_test
async def test_manual_revocation_changes_mark_snapshot_dirty(self) -> None:
from dataclasses import replace
from unittest.mock import patch
from kca import ocsp
from kca.models import OcspSnapshot
async with api_client() as client:
with oidc_user("alice"):
vault_id = await _vault(client)
ca_key = await _key(client, vault_id, "manual dirty ca")
ca = await _cert(client, vault_id, ca_key["id"], "Manual Dirty CA", ca=True)
configured = replace(ocsp.settings, ocsp_s3_bucket="test-bucket")
with patch.object(ocsp, "settings", configured):
await OcspSnapshot.create(ca_id=ca["id"], dirty=False)
response = await client.post(
f"/api/vaults/{vault_id}/certificates/{ca['id']}/revocations",
json={"serial_hex": "1234", "reason": "keyCompromise"},
)
assert response.status_code == 201, response.text
revocation = response.json()
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is True
await OcspSnapshot.filter(ca_id=ca["id"]).update(dirty=False)
response = await client.patch(
f"/api/vaults/{vault_id}/certificates/{ca['id']}/revocations/{revocation['id']}",
json={"reason": "superseded"},
)
assert response.status_code == 200, response.text
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is True
await OcspSnapshot.filter(ca_id=ca["id"]).update(dirty=False)
response = await client.delete(
f"/api/vaults/{vault_id}/certificates/{ca['id']}/revocations/{revocation['id']}"
)
assert response.status_code == 204, response.text
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is True
@async_test
async def test_publisher_preserves_changes_during_publication(self) -> None:
from dataclasses import replace
from unittest.mock import patch
from kca import ocsp
from kca.models import CertificateRevocation, OcspSnapshot
async with api_client() as client:
with oidc_user("alice"):
vault_id = await _vault(client)
ca_key = await _key(client, vault_id, "concurrent publish ca")
ca = await _cert(client, vault_id, ca_key["id"], "Concurrent Publish CA", ca=True)
leaf_key = await _key(client, vault_id, "concurrent publish leaf")
leaf = await _cert(
client, vault_id, leaf_key["id"], "Concurrent Publish Leaf", signer_id=ca["id"]
)
published = []
async def fake_publish(publish_ca, snapshot):
published.append(snapshot)
if len(published) == 1:
await CertificateRevocation.create(
ca_id=publish_ca.id,
serial_hex=leaf["serial_hex"],
revoked_at=datetime.now(timezone.utc),
reason="keyCompromise",
)
await ocsp.mark_ocsp_dirty(publish_ca.id)
return ocsp.object_key(str(publish_ca.id))
configured = replace(ocsp.settings, ocsp_s3_bucket="test-bucket")
with patch.object(ocsp, "settings", configured), patch.object(
ocsp, "publish_snapshot", fake_publish
):
await ocsp.mark_ocsp_dirty(ca["id"])
async def ensure_context():
return None
mixin = ocsp.OcspPublisherMixin(1, ensure_context)
await mixin.run_once()
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is True
await mixin.run_once()
assert len(published) == 2
first_entries = {entry["serial_hex"]: entry for entry in published[0]["entries"]}
second_entries = {entry["serial_hex"]: entry for entry in published[1]["entries"]}
serial = leaf["serial_hex"].lower()
assert first_entries[serial]["status"] == "valid"
assert second_entries[serial]["status"] == "revoked"
assert second_entries[serial]["reason"] == "keyCompromise"
row = await OcspSnapshot.get(ca_id=ca["id"])
assert row.dirty is False
@async_test
async def test_publisher_marks_dirty_and_uploads(self) -> None:
from dataclasses import replace