feat: add confidential ballot reference provider
This commit is contained in:
@@ -0,0 +1,761 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from datetime import UTC, datetime
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.encryption import (
|
||||
ContentProtectionRequest,
|
||||
ContentUnprotectionRequest,
|
||||
KeyVaultCreateRequest,
|
||||
encryption_content_cipher,
|
||||
encryption_key_vault,
|
||||
)
|
||||
from govoplan_core.core.voting import (
|
||||
ExternalVotingBallotRef,
|
||||
ExternalVotingCastRequest,
|
||||
ExternalVotingFinalizationRequest,
|
||||
ExternalVotingPreparationRequest,
|
||||
VotingReceipt,
|
||||
VotingResult,
|
||||
)
|
||||
from govoplan_voting.backend.db.models import (
|
||||
VotingConfidentialBallot,
|
||||
VotingConfidentialCast,
|
||||
)
|
||||
|
||||
|
||||
LOCAL_CONFIDENTIAL_PROVIDER_ID = "local_confidential"
|
||||
LOCAL_KEY_PROVIDER_ID = "local_aesgcm"
|
||||
|
||||
|
||||
class LocalConfidentialVotingError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class LocalConfidentialVotingProvider:
|
||||
"""Reference confidential provider backed by encrypted shared SQL state.
|
||||
|
||||
The provider conceals selections at rest from ordinary Voting storage. It is
|
||||
server-decryptable and intentionally makes no secret-ballot, coercion-
|
||||
resistance, anonymity, HSM, or external-certification claim.
|
||||
"""
|
||||
|
||||
def __init__(self, registry: object | None) -> None:
|
||||
self._registry = registry
|
||||
|
||||
def prepare_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: ExternalVotingPreparationRequest,
|
||||
) -> ExternalVotingBallotRef:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, request.tenant_id)
|
||||
if request.assurance_profile != "confidential":
|
||||
raise LocalConfidentialVotingError(
|
||||
"The local confidential provider supports only the confidential assurance profile."
|
||||
)
|
||||
provider_ref = (
|
||||
str(request.requested_provider_ballot_ref or "").strip()
|
||||
or f"local-confidential:{request.ballot_id}"
|
||||
)
|
||||
payload = _preparation_payload(request, provider_ref=provider_ref)
|
||||
digest = _digest(payload)
|
||||
existing = (
|
||||
db.query(VotingConfidentialBallot)
|
||||
.filter(
|
||||
VotingConfidentialBallot.tenant_id == request.tenant_id,
|
||||
VotingConfidentialBallot.provider_ballot_ref == provider_ref,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if existing is not None:
|
||||
if existing.preparation_request_sha256 != digest:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider ballot reference was reused for another definition."
|
||||
)
|
||||
return self._ballot_ref(existing, replayed=True)
|
||||
|
||||
cipher = encryption_content_cipher(self._registry)
|
||||
key_vault = encryption_key_vault(self._registry)
|
||||
if cipher is None or key_vault is None:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The local confidential provider requires the Encryption key-vault and content-cipher capabilities."
|
||||
)
|
||||
vault_id = f"voting-{request.ballot_id}"
|
||||
vault = key_vault.get_vault(
|
||||
db,
|
||||
tenant_id=request.tenant_id,
|
||||
vault_id=vault_id,
|
||||
)
|
||||
if vault is None:
|
||||
vault = key_vault.create_vault(
|
||||
db,
|
||||
principal,
|
||||
request=KeyVaultCreateRequest(
|
||||
tenant_id=request.tenant_id,
|
||||
vault_id=vault_id,
|
||||
name=f"Confidential ballot {request.ballot_id}",
|
||||
provider_id=LOCAL_KEY_PROVIDER_ID,
|
||||
purpose="voting.confidential.ballot",
|
||||
algorithm_suite="AES-256-GCM",
|
||||
scope_type="voting_ballot",
|
||||
scope_id=request.ballot_id,
|
||||
policy_ref="voting:local-confidential:v1",
|
||||
idempotency_key=f"voting-vault:{request.ballot_id}",
|
||||
recovery_quorum=2,
|
||||
profile_kind="server_envelope",
|
||||
),
|
||||
)
|
||||
if vault.state != "active" or vault.current_key is None:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential ballot Encryption vault is not active."
|
||||
)
|
||||
row = VotingConfidentialBallot(
|
||||
tenant_id=request.tenant_id,
|
||||
provider_ballot_ref=provider_ref,
|
||||
ballot_id=request.ballot_id,
|
||||
definition_sha256=request.definition_sha256,
|
||||
electorate_sha256=request.electorate_sha256,
|
||||
assurance_profile=request.assurance_profile,
|
||||
method=request.method,
|
||||
state="open",
|
||||
options=[asdict(item) for item in request.options],
|
||||
electorate=[
|
||||
{
|
||||
"subject_id": item.subject_id,
|
||||
"label": item.label,
|
||||
"weight": item.weight,
|
||||
"provenance": dict(item.provenance),
|
||||
}
|
||||
for item in request.electorate
|
||||
],
|
||||
allow_replacement=request.allow_replacement,
|
||||
quorum_weight=request.quorum_weight,
|
||||
threshold_numerator=request.threshold_numerator,
|
||||
threshold_denominator=request.threshold_denominator,
|
||||
opens_at=request.opens_at,
|
||||
closes_at=request.closes_at,
|
||||
vault_id=vault_id,
|
||||
preparation_idempotency_key=request.idempotency_key,
|
||||
preparation_request_sha256=digest,
|
||||
prepared_at=request.requested_at,
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return self._ballot_ref(row)
|
||||
|
||||
def cast_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: ExternalVotingCastRequest,
|
||||
) -> VotingReceipt:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, request.tenant_id)
|
||||
actor_id = _principal_actor(principal)
|
||||
if not actor_id or actor_id != request.elector_id:
|
||||
raise LocalConfidentialVotingError(
|
||||
"A confidential ballot principal cannot cast for another elector."
|
||||
)
|
||||
ballot = self._ballot(
|
||||
db,
|
||||
tenant_id=request.tenant_id,
|
||||
provider_ballot_ref=request.provider_ballot_ref,
|
||||
lock=True,
|
||||
)
|
||||
self._validate_binding(
|
||||
ballot,
|
||||
ballot_id=request.ballot_id,
|
||||
definition_sha256=request.definition_sha256,
|
||||
electorate_sha256=request.electorate_sha256,
|
||||
)
|
||||
if ballot.state != "open":
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider ballot is not open."
|
||||
)
|
||||
_validate_window(ballot)
|
||||
electorate = {str(item["subject_id"]): item for item in ballot.electorate}
|
||||
elector = electorate.get(request.elector_id)
|
||||
if elector is None:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The current principal is not in the confidential provider electorate."
|
||||
)
|
||||
selections = tuple(str(item).strip() for item in request.selections)
|
||||
_validate_selections(ballot, selections)
|
||||
if request.allow_replacement != ballot.allow_replacement:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider replacement policy does not match the frozen ballot."
|
||||
)
|
||||
cipher = encryption_content_cipher(self._registry)
|
||||
if cipher is None:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider cannot cast because Encryption is unavailable."
|
||||
)
|
||||
replay = (
|
||||
db.query(VotingConfidentialCast)
|
||||
.filter(
|
||||
VotingConfidentialCast.tenant_id == request.tenant_id,
|
||||
VotingConfidentialCast.provider_ballot_id == ballot.id,
|
||||
VotingConfidentialCast.idempotency_key == request.idempotency_key,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if replay is not None:
|
||||
opened = cipher.unprotect_content(
|
||||
db,
|
||||
request=ContentUnprotectionRequest(
|
||||
tenant_id=request.tenant_id,
|
||||
owner_module="voting",
|
||||
resource_type="voting_confidential_cast",
|
||||
resource_id=replay.encryption_resource_id,
|
||||
envelope_id=replay.encryption_envelope_id,
|
||||
ciphertext=replay.ciphertext,
|
||||
actor_id=actor_id,
|
||||
),
|
||||
)
|
||||
stored_selections = _validate_decrypted_cast(
|
||||
replay,
|
||||
ballot,
|
||||
json.loads(opened.decode("utf-8")),
|
||||
)
|
||||
request_digest = _confidential_cast_request_digest(
|
||||
request,
|
||||
generation=replay.generation,
|
||||
ciphertext=replay.ciphertext,
|
||||
)
|
||||
if replay.request_sha256 != request_digest:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential vote idempotency key was reused for another vote."
|
||||
)
|
||||
if stored_selections != selections:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential vote idempotency key was reused for another vote."
|
||||
)
|
||||
return VotingReceipt(
|
||||
ballot_id=request.ballot_id,
|
||||
revision=0,
|
||||
receipt_sha256=replay.receipt_sha256,
|
||||
cast_at=_aware(replay.cast_at),
|
||||
replaced_previous=replay.generation > 1,
|
||||
replayed=True,
|
||||
)
|
||||
previous = (
|
||||
db.query(VotingConfidentialCast)
|
||||
.filter(
|
||||
VotingConfidentialCast.tenant_id == request.tenant_id,
|
||||
VotingConfidentialCast.provider_ballot_id == ballot.id,
|
||||
VotingConfidentialCast.elector_id == request.elector_id,
|
||||
VotingConfidentialCast.superseded_at.is_(None),
|
||||
)
|
||||
.with_for_update()
|
||||
.one_or_none()
|
||||
)
|
||||
if previous is not None and not ballot.allow_replacement:
|
||||
raise LocalConfidentialVotingError(
|
||||
"This confidential ballot does not allow vote replacement."
|
||||
)
|
||||
generation = previous.generation + 1 if previous is not None else 1
|
||||
cast_at = request.requested_at
|
||||
cast_id = str(uuid.uuid4())
|
||||
plaintext = _confidential_cast_plaintext(
|
||||
request,
|
||||
generation=generation,
|
||||
selections=selections,
|
||||
)
|
||||
protected = cipher.protect_content(
|
||||
db,
|
||||
request=ContentProtectionRequest(
|
||||
tenant_id=request.tenant_id,
|
||||
owner_module="voting",
|
||||
resource_type="voting_confidential_cast",
|
||||
resource_id=cast_id,
|
||||
profile_id="voting-local-confidential-v1",
|
||||
vault_id=ballot.vault_id,
|
||||
ciphertext_ref=f"voting-db://confidential-casts/{cast_id}",
|
||||
plaintext=plaintext,
|
||||
policy_decision_ref="voting:local-confidential:v1",
|
||||
idempotency_key=f"voting-confidential-cast:{cast_id}",
|
||||
actor_id=actor_id,
|
||||
metadata={"ballot_id": request.ballot_id, "generation": generation},
|
||||
),
|
||||
)
|
||||
request_digest = _confidential_cast_request_digest(
|
||||
request,
|
||||
generation=generation,
|
||||
ciphertext=protected.ciphertext,
|
||||
)
|
||||
receipt_hash = _digest(
|
||||
{
|
||||
"tenant_id": request.tenant_id,
|
||||
"ballot_id": request.ballot_id,
|
||||
"definition_sha256": request.definition_sha256,
|
||||
"elector_id": request.elector_id,
|
||||
"generation": generation,
|
||||
"ciphertext_digest": protected.envelope.ciphertext_digest,
|
||||
"cast_at": cast_at.isoformat(),
|
||||
"idempotency_key": request.idempotency_key,
|
||||
}
|
||||
)
|
||||
if previous is not None:
|
||||
previous.superseded_at = cast_at
|
||||
db.add(
|
||||
VotingConfidentialCast(
|
||||
id=cast_id,
|
||||
tenant_id=request.tenant_id,
|
||||
provider_ballot_id=ballot.id,
|
||||
elector_id=request.elector_id,
|
||||
generation=generation,
|
||||
definition_sha256=request.definition_sha256,
|
||||
ciphertext=protected.ciphertext,
|
||||
encryption_envelope_id=protected.envelope.envelope_id,
|
||||
encryption_resource_id=cast_id,
|
||||
weight=int(elector.get("weight") or 1),
|
||||
cast_at=cast_at,
|
||||
idempotency_key=request.idempotency_key,
|
||||
request_sha256=request_digest,
|
||||
receipt_sha256=receipt_hash,
|
||||
)
|
||||
)
|
||||
db.flush()
|
||||
return VotingReceipt(
|
||||
ballot_id=request.ballot_id,
|
||||
revision=0,
|
||||
receipt_sha256=receipt_hash,
|
||||
cast_at=cast_at,
|
||||
replaced_previous=previous is not None,
|
||||
)
|
||||
|
||||
def finalize_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: ExternalVotingFinalizationRequest,
|
||||
) -> VotingResult:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, request.tenant_id)
|
||||
ballot = self._ballot(
|
||||
db,
|
||||
tenant_id=request.tenant_id,
|
||||
provider_ballot_ref=request.provider_ballot_ref,
|
||||
lock=True,
|
||||
)
|
||||
self._validate_binding(
|
||||
ballot,
|
||||
ballot_id=request.ballot_id,
|
||||
definition_sha256=request.definition_sha256,
|
||||
electorate_sha256=request.electorate_sha256,
|
||||
)
|
||||
if ballot.result is not None:
|
||||
return _result_from_mapping(ballot.result)
|
||||
if ballot.state != "open":
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider ballot cannot be finalized from its current state."
|
||||
)
|
||||
option_keys = tuple(str(item["key"]) for item in ballot.options)
|
||||
if option_keys != tuple(item.key for item in request.options):
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider options do not match the frozen ballot."
|
||||
)
|
||||
if request.eligible_count != len(
|
||||
ballot.electorate
|
||||
) or request.eligible_weight != sum(
|
||||
int(item.get("weight") or 1) for item in ballot.electorate
|
||||
):
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider electorate totals do not match the frozen ballot."
|
||||
)
|
||||
cipher = encryption_content_cipher(self._registry)
|
||||
if cipher is None:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider cannot tally because Encryption is unavailable."
|
||||
)
|
||||
rows = (
|
||||
db.query(VotingConfidentialCast)
|
||||
.filter(
|
||||
VotingConfidentialCast.tenant_id == request.tenant_id,
|
||||
VotingConfidentialCast.provider_ballot_id == ballot.id,
|
||||
VotingConfidentialCast.superseded_at.is_(None),
|
||||
)
|
||||
.order_by(VotingConfidentialCast.id.asc())
|
||||
.all()
|
||||
)
|
||||
counts = {key: 0 for key in option_keys}
|
||||
weighted = {key: 0 for key in option_keys}
|
||||
for row in rows:
|
||||
opened = cipher.unprotect_content(
|
||||
db,
|
||||
request=ContentUnprotectionRequest(
|
||||
tenant_id=request.tenant_id,
|
||||
owner_module="voting",
|
||||
resource_type="voting_confidential_cast",
|
||||
resource_id=row.encryption_resource_id,
|
||||
envelope_id=row.encryption_envelope_id,
|
||||
ciphertext=row.ciphertext,
|
||||
actor_id=_principal_actor(principal),
|
||||
),
|
||||
)
|
||||
item = json.loads(opened.decode("utf-8"))
|
||||
selections = _validate_decrypted_cast(row, ballot, item)
|
||||
for selection in selections:
|
||||
counts[selection] += 1
|
||||
weighted[selection] += row.weight
|
||||
cast_weight = sum(row.weight for row in rows)
|
||||
eligible_weight = sum(
|
||||
int(item.get("weight") or 1) for item in ballot.electorate
|
||||
)
|
||||
quorum_met = cast_weight >= ballot.quorum_weight
|
||||
max_weight = max(weighted.values(), default=0)
|
||||
winners = tuple(
|
||||
key for key in option_keys if max_weight > 0 and weighted[key] == max_weight
|
||||
)
|
||||
threshold_met = bool(
|
||||
quorum_met
|
||||
and winners
|
||||
and max_weight * ballot.threshold_denominator
|
||||
>= max(cast_weight, 1) * ballot.threshold_numerator
|
||||
)
|
||||
result_body = {
|
||||
"ballot_id": ballot.ballot_id,
|
||||
"counts": counts,
|
||||
"weighted_counts": weighted,
|
||||
"cast_count": len(rows),
|
||||
"cast_weight": cast_weight,
|
||||
"eligible_count": len(ballot.electorate),
|
||||
"eligible_weight": eligible_weight,
|
||||
"quorum_met": quorum_met,
|
||||
"threshold_met": threshold_met,
|
||||
"winning_options": winners,
|
||||
"definition_sha256": ballot.definition_sha256,
|
||||
"electorate_sha256": ballot.electorate_sha256,
|
||||
}
|
||||
result_sha256 = _digest(result_body)
|
||||
evidence = (
|
||||
{
|
||||
"kind": "reference_provider_result",
|
||||
"provider_id": LOCAL_CONFIDENTIAL_PROVIDER_ID,
|
||||
"provider_ballot_ref": ballot.provider_ballot_ref,
|
||||
"assurance_profile": "confidential",
|
||||
"protection_profile": "server_envelope",
|
||||
"definition_sha256": ballot.definition_sha256,
|
||||
"electorate_sha256": ballot.electorate_sha256,
|
||||
"active_receipts_sha256": _digest(
|
||||
sorted(row.receipt_sha256 for row in rows)
|
||||
),
|
||||
"certified": False,
|
||||
},
|
||||
)
|
||||
result = VotingResult(
|
||||
ballot_id=ballot.ballot_id,
|
||||
revision=0,
|
||||
counts=counts,
|
||||
weighted_counts=weighted,
|
||||
cast_count=len(rows),
|
||||
cast_weight=cast_weight,
|
||||
eligible_count=len(ballot.electorate),
|
||||
eligible_weight=eligible_weight,
|
||||
quorum_met=quorum_met,
|
||||
threshold_met=threshold_met,
|
||||
winning_options=winners,
|
||||
result_sha256=result_sha256,
|
||||
evidence=evidence,
|
||||
)
|
||||
ballot.state = "closed"
|
||||
ballot.finalization_idempotency_key = request.idempotency_key
|
||||
ballot.finalized_at = request.requested_at
|
||||
ballot.result = _result_mapping(result)
|
||||
db.flush()
|
||||
return result
|
||||
|
||||
def ballot_status(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
provider_ballot_ref: str,
|
||||
) -> ExternalVotingBallotRef | None:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, tenant_id)
|
||||
row = (
|
||||
db.query(VotingConfidentialBallot)
|
||||
.filter(
|
||||
VotingConfidentialBallot.tenant_id == tenant_id,
|
||||
VotingConfidentialBallot.provider_ballot_ref == provider_ballot_ref,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
return self._ballot_ref(row) if row is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _validate_binding(
|
||||
ballot: VotingConfidentialBallot,
|
||||
*,
|
||||
ballot_id: str,
|
||||
definition_sha256: str,
|
||||
electorate_sha256: str,
|
||||
) -> None:
|
||||
if (
|
||||
ballot.ballot_id != ballot_id
|
||||
or ballot.definition_sha256 != definition_sha256
|
||||
or ballot.electorate_sha256 != electorate_sha256
|
||||
):
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider request does not match the frozen ballot."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _ballot_ref(
|
||||
row: VotingConfidentialBallot,
|
||||
*,
|
||||
replayed: bool = False,
|
||||
) -> ExternalVotingBallotRef:
|
||||
return ExternalVotingBallotRef(
|
||||
provider_id=LOCAL_CONFIDENTIAL_PROVIDER_ID,
|
||||
provider_ballot_ref=row.provider_ballot_ref,
|
||||
state="closed" if row.state == "closed" else "open",
|
||||
definition_sha256=row.definition_sha256,
|
||||
electorate_sha256=row.electorate_sha256,
|
||||
evidence=(
|
||||
{
|
||||
"kind": "reference_provider_preparation",
|
||||
"assurance_profile": "confidential",
|
||||
"protection_profile": "server_envelope",
|
||||
"replayed": replayed,
|
||||
"certified": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _ballot(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
provider_ballot_ref: str,
|
||||
lock: bool,
|
||||
) -> VotingConfidentialBallot:
|
||||
query = session.query(VotingConfidentialBallot).filter(
|
||||
VotingConfidentialBallot.tenant_id == tenant_id,
|
||||
VotingConfidentialBallot.provider_ballot_ref == provider_ballot_ref,
|
||||
)
|
||||
if lock:
|
||||
query = query.with_for_update()
|
||||
row = query.one_or_none()
|
||||
if row is None:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider ballot was not found."
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
def _preparation_payload(
|
||||
request: ExternalVotingPreparationRequest,
|
||||
*,
|
||||
provider_ref: str,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"tenant_id": request.tenant_id,
|
||||
"ballot_id": request.ballot_id,
|
||||
"provider_ballot_ref": provider_ref,
|
||||
"definition_sha256": request.definition_sha256,
|
||||
"electorate_sha256": request.electorate_sha256,
|
||||
"assurance_profile": request.assurance_profile,
|
||||
"method": request.method,
|
||||
"options": [asdict(item) for item in request.options],
|
||||
"electorate": [asdict(item) for item in request.electorate],
|
||||
"allow_replacement": request.allow_replacement,
|
||||
"quorum_weight": request.quorum_weight,
|
||||
"threshold_numerator": request.threshold_numerator,
|
||||
"threshold_denominator": request.threshold_denominator,
|
||||
"opens_at": request.opens_at,
|
||||
"closes_at": request.closes_at,
|
||||
"idempotency_key": request.idempotency_key,
|
||||
}
|
||||
|
||||
|
||||
def _validate_selections(
|
||||
ballot: VotingConfidentialBallot,
|
||||
selections: tuple[str, ...],
|
||||
) -> None:
|
||||
option_keys = {str(item["key"]) for item in ballot.options}
|
||||
if (
|
||||
not selections
|
||||
or len(selections) != len(set(selections))
|
||||
or not set(selections) <= option_keys
|
||||
):
|
||||
raise LocalConfidentialVotingError(
|
||||
"Confidential selections must be unique configured option keys."
|
||||
)
|
||||
if ballot.method in {"single_choice", "yes_no_abstain"} and len(selections) != 1:
|
||||
raise LocalConfidentialVotingError(
|
||||
"This confidential Voting method requires exactly one selection."
|
||||
)
|
||||
|
||||
|
||||
def _validate_decrypted_cast(
|
||||
row: VotingConfidentialCast,
|
||||
ballot: VotingConfidentialBallot,
|
||||
value: object,
|
||||
) -> tuple[str, ...]:
|
||||
if not isinstance(value, dict):
|
||||
raise LocalConfidentialVotingError(
|
||||
"A confidential cast payload has an invalid structure."
|
||||
)
|
||||
selections = tuple(str(item) for item in value.get("selections", ()))
|
||||
if (
|
||||
value.get("ballot_id") != ballot.ballot_id
|
||||
or value.get("definition_sha256") != ballot.definition_sha256
|
||||
or value.get("elector_id") != row.elector_id
|
||||
or value.get("generation") != row.generation
|
||||
):
|
||||
raise LocalConfidentialVotingError(
|
||||
"A confidential cast payload does not match its authenticated record."
|
||||
)
|
||||
_validate_selections(ballot, selections)
|
||||
return selections
|
||||
|
||||
|
||||
def _confidential_cast_plaintext(
|
||||
request: ExternalVotingCastRequest,
|
||||
*,
|
||||
generation: int,
|
||||
selections: tuple[str, ...],
|
||||
) -> bytes:
|
||||
return _canonical_json(
|
||||
{
|
||||
"ballot_id": request.ballot_id,
|
||||
"definition_sha256": request.definition_sha256,
|
||||
"elector_id": request.elector_id,
|
||||
"generation": generation,
|
||||
"selections": selections,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _confidential_cast_request_digest(
|
||||
request: ExternalVotingCastRequest,
|
||||
*,
|
||||
generation: int,
|
||||
ciphertext: bytes,
|
||||
) -> str:
|
||||
return _digest(
|
||||
{
|
||||
"tenant_id": request.tenant_id,
|
||||
"ballot_id": request.ballot_id,
|
||||
"provider_ballot_ref": request.provider_ballot_ref,
|
||||
"definition_sha256": request.definition_sha256,
|
||||
"electorate_sha256": request.electorate_sha256,
|
||||
"elector_id": request.elector_id,
|
||||
"allow_replacement": request.allow_replacement,
|
||||
"generation": generation,
|
||||
"ciphertext_sha256": hashlib.sha256(ciphertext).hexdigest(),
|
||||
"idempotency_key": request.idempotency_key,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_window(ballot: VotingConfidentialBallot) -> None:
|
||||
now = datetime.now(UTC)
|
||||
if ballot.opens_at is not None and now < _aware(ballot.opens_at):
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider ballot has not opened yet."
|
||||
)
|
||||
if ballot.closes_at is not None and now >= _aware(ballot.closes_at):
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider ballot has reached its close time."
|
||||
)
|
||||
|
||||
|
||||
def _result_mapping(result: VotingResult) -> dict[str, Any]:
|
||||
return {
|
||||
"ballot_id": result.ballot_id,
|
||||
"revision": result.revision,
|
||||
"counts": dict(result.counts),
|
||||
"weighted_counts": dict(result.weighted_counts),
|
||||
"cast_count": result.cast_count,
|
||||
"cast_weight": result.cast_weight,
|
||||
"eligible_count": result.eligible_count,
|
||||
"eligible_weight": result.eligible_weight,
|
||||
"quorum_met": result.quorum_met,
|
||||
"threshold_met": result.threshold_met,
|
||||
"winning_options": list(result.winning_options),
|
||||
"result_sha256": result.result_sha256,
|
||||
"evidence": [dict(item) for item in result.evidence],
|
||||
}
|
||||
|
||||
|
||||
def _result_from_mapping(value: dict[str, Any]) -> VotingResult:
|
||||
return VotingResult(
|
||||
ballot_id=str(value["ballot_id"]),
|
||||
revision=int(value.get("revision") or 0),
|
||||
counts={str(key): int(item) for key, item in value["counts"].items()},
|
||||
weighted_counts={
|
||||
str(key): int(item) for key, item in value["weighted_counts"].items()
|
||||
},
|
||||
cast_count=int(value["cast_count"]),
|
||||
cast_weight=int(value["cast_weight"]),
|
||||
eligible_count=int(value["eligible_count"]),
|
||||
eligible_weight=int(value["eligible_weight"]),
|
||||
quorum_met=bool(value["quorum_met"]),
|
||||
threshold_met=bool(value["threshold_met"]),
|
||||
winning_options=tuple(str(item) for item in value["winning_options"]),
|
||||
result_sha256=str(value["result_sha256"]),
|
||||
evidence=tuple(dict(item) for item in value.get("evidence", ())),
|
||||
)
|
||||
|
||||
|
||||
def _require_tenant(principal: object, tenant_id: str) -> None:
|
||||
if str(getattr(principal, "tenant_id", "") or "").strip() != tenant_id:
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider principal belongs to another tenant."
|
||||
)
|
||||
|
||||
|
||||
def _principal_actor(principal: object) -> str | None:
|
||||
for name in ("account_id", "user_id", "identity_id", "membership_id", "subject"):
|
||||
value = str(getattr(principal, name, "") or "").strip()
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _session(value: object) -> Session:
|
||||
if not hasattr(value, "query"):
|
||||
raise LocalConfidentialVotingError(
|
||||
"The confidential provider requires a database session."
|
||||
)
|
||||
return value # type: ignore[return-value]
|
||||
|
||||
|
||||
def _aware(value: datetime) -> datetime:
|
||||
return value if value.tzinfo is not None else value.replace(tzinfo=UTC)
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
default=str,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _digest(value: object) -> str:
|
||||
return hashlib.sha256(_canonical_json(value)).hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LOCAL_CONFIDENTIAL_PROVIDER_ID",
|
||||
"LocalConfidentialVotingError",
|
||||
"LocalConfidentialVotingProvider",
|
||||
]
|
||||
Reference in New Issue
Block a user