feat: add confidential ballot reference provider

This commit is contained in:
2026-08-02 03:41:01 +02:00
parent c176c5cfcb
commit 0a4e06077d
10 changed files with 1505 additions and 44 deletions
+192 -1
View File
@@ -1,6 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import UTC, datetime
import hashlib
from types import SimpleNamespace
import unittest
from sqlalchemy import create_engine
@@ -11,9 +14,23 @@ from govoplan_core.core.voting import (
VotingCastCommand,
VotingElector,
VotingOption,
voting_provider_capability,
)
from govoplan_core.core.encryption import (
CAPABILITY_ENCRYPTION_CONTENT_CIPHER,
CAPABILITY_ENCRYPTION_KEY_VAULT,
ContentProtectionEnvelope,
ProtectedContent,
)
from govoplan_core.db.base import Base
from govoplan_voting.backend.db.models import VotingCastRecord
from govoplan_voting.backend.db.models import (
VotingCastRecord,
VotingConfidentialCast,
)
from govoplan_voting.backend.local_confidential_provider import (
LOCAL_CONFIDENTIAL_PROVIDER_ID,
LocalConfidentialVotingProvider,
)
from govoplan_voting.backend.service import SqlVotingBallots, VotingStoreError
@@ -23,6 +40,87 @@ class Principal:
account_id: str
class FakeRegistry:
def __init__(self) -> None:
self.capabilities: dict[str, object] = {}
def has_capability(self, name: str) -> bool:
return name in self.capabilities
def capability(self, name: str) -> object | None:
return self.capabilities.get(name)
class FakeKeyVault:
def __init__(self) -> None:
self.vaults: dict[tuple[str, str], object] = {}
def create_vault(self, session, principal, *, request):
del session, principal
vault = SimpleNamespace(state="active", current_key=SimpleNamespace(version=1))
self.vaults[(request.tenant_id, request.vault_id)] = vault
return vault
def get_vault(self, session, *, tenant_id, vault_id):
del session
return self.vaults.get((tenant_id, vault_id))
def rotate_key(self, *args, **kwargs):
raise NotImplementedError
def revoke_key(self, *args, **kwargs):
raise NotImplementedError
def schedule_destruction(self, *args, **kwargs):
raise NotImplementedError
def reconcile_vault(self, *args, **kwargs):
raise NotImplementedError
class FakeContentCipher:
def __init__(self) -> None:
self.plaintexts: dict[str, bytes] = {}
def protect_content(self, session, *, request):
del session
envelope_id = f"envelope-{request.resource_id}"
ciphertext = b"ciphertext:" + hashlib.sha256(request.plaintext).digest()
self.plaintexts[envelope_id] = request.plaintext
return ProtectedContent(
envelope=ContentProtectionEnvelope(
envelope_id=envelope_id,
tenant_id=request.tenant_id,
owner_module=request.owner_module,
resource_type=request.resource_type,
resource_id=request.resource_id,
profile_kind="server_envelope",
profile_id=request.profile_id,
provider_id="fake",
vault_id=request.vault_id,
key_version=1,
algorithm_suite="AES-256-GCM",
ciphertext_ref=request.ciphertext_ref,
ciphertext_digest=hashlib.sha256(ciphertext).hexdigest(),
authenticated_context_digest="a" * 64,
state="active",
created_at=datetime.now(UTC),
wrapped_key_refs=(f"wrapped-{request.resource_id}",),
),
ciphertext=ciphertext,
)
def unprotect_content(self, session, *, request):
del session
return self.plaintexts[request.envelope_id]
def execute_rewrap(self, *args, **kwargs):
raise NotImplementedError
def prepare_reencryption(self, *args, **kwargs):
raise NotImplementedError
def command(*, assurance: str = "recorded", provider_id: str | None = None):
return VotingBallotCreateCommand(
title="Budget choice",
@@ -192,6 +290,99 @@ class VotingTests(unittest.TestCase):
idempotency_key="open-secret",
)
def test_local_confidential_provider_encrypts_casts_and_returns_aggregates(
self,
) -> None:
registry = FakeRegistry()
provider = LocalConfidentialVotingProvider(registry)
registry.capabilities.update(
{
voting_provider_capability(LOCAL_CONFIDENTIAL_PROVIDER_ID): provider,
CAPABILITY_ENCRYPTION_KEY_VAULT: FakeKeyVault(),
CAPABILITY_ENCRYPTION_CONTENT_CIPHER: FakeContentCipher(),
}
)
service = SqlVotingBallots(registry)
draft = replace(
command(
assurance="confidential",
provider_id=LOCAL_CONFIDENTIAL_PROVIDER_ID,
),
provider_ballot_ref=None,
)
with self.Session() as session:
created = service.create_ballot(
session,
self.manager,
command=draft,
idempotency_key="create-confidential",
)
opened = service.open_ballot(
session,
self.manager,
ballot_id=created.id,
expected_revision=created.revision,
idempotency_key="open-confidential",
)
detail = service.get_ballot(
session,
self.manager,
ballot_id=opened.id,
)
self.assertEqual(
f"local-confidential:{opened.id}",
detail["provider_ballot_ref"],
)
first = service.cast_ballot(
session,
Principal("tenant-1", "alice"),
ballot_id=opened.id,
command=VotingCastCommand(("a",), "cast-confidential-alice"),
)
replay = service.cast_ballot(
session,
Principal("tenant-1", "alice"),
ballot_id=opened.id,
command=VotingCastCommand(("a",), "cast-confidential-alice"),
)
self.assertEqual(first.receipt_sha256, replay.receipt_sha256)
self.assertTrue(replay.replayed)
with self.assertRaisesRegex(
VotingStoreError,
"provider cast was rejected",
):
service.cast_ballot(
session,
Principal("tenant-1", "alice"),
ballot_id=opened.id,
command=VotingCastCommand(
("b",),
"cast-confidential-alice",
),
)
service.cast_ballot(
session,
Principal("tenant-1", "bob"),
ballot_id=opened.id,
command=VotingCastCommand(("b",), "cast-confidential-bob"),
)
self.assertEqual(0, session.query(VotingCastRecord).count())
encrypted = session.query(VotingConfidentialCast).all()
self.assertEqual(2, len(encrypted))
self.assertTrue(all(row.ciphertext for row in encrypted))
result = service.close_ballot(
session,
self.manager,
ballot_id=opened.id,
expected_revision=opened.revision,
idempotency_key="close-confidential",
)
self.assertEqual({"a": 1, "b": 1}, dict(result.counts))
self.assertEqual({"a": 2, "b": 1}, dict(result.weighted_counts))
self.assertEqual("local_confidential", result.evidence[0]["provider_id"])
self.assertFalse(result.evidence[0]["certified"])
def test_idempotency_key_cannot_change_command(self) -> None:
with self.Session() as session:
self.service.create_ballot(