Add Voting provider assurance contract

This commit is contained in:
2026-08-04 14:01:05 +02:00
parent bca3e46293
commit 2b5c14385d
2 changed files with 262 additions and 2 deletions
+196 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from datetime import UTC, datetime
import json
from typing import Literal, Protocol, runtime_checkable
@@ -15,6 +15,20 @@ VOTING_ASSURANCE_CONFIDENTIAL = "confidential"
VOTING_ASSURANCE_SECRET = "secret"
VOTING_ASSURANCE_EXTERNAL_CERTIFIED = "external_certified"
VOTING_CERTIFICATION_NOT_CERTIFIED = "not_certified"
VOTING_CERTIFICATION_IN_EVALUATION = "in_evaluation"
VOTING_CERTIFICATION_CERTIFIED = "certified"
VOTING_CERTIFICATION_EXPIRED = "expired"
VOTING_CERTIFICATION_REVOKED = "revoked"
VotingProviderCertificationState = Literal[
"not_certified",
"in_evaluation",
"certified",
"expired",
"revoked",
]
class VotingCapabilityError(ValueError):
"""Stable error raised by Voting capability implementations."""
@@ -30,6 +44,119 @@ def voting_provider_capability(provider_id: str) -> str:
return f"{CAPABILITY_VOTING_PROVIDER_PREFIX}{normalized}"
@dataclass(frozen=True, slots=True)
class VotingProviderAssuranceDeclaration:
"""Pinned assurance and certification claim made by a Voting provider."""
provider_id: str
implementation_ref: str
supported_assurance_profiles: tuple[
Literal["confidential", "secret", "external_certified"], ...
]
certification_state: VotingProviderCertificationState
protocol_ref: str
protocol_version: str
certification_authority: str | None = None
certification_reference: str | None = None
certification_evidence_ref: str | None = None
certification_valid_from: datetime | None = None
certification_valid_until: datetime | None = None
notes: tuple[str, ...] = ()
def __post_init__(self) -> None:
normalized_id = str(self.provider_id or "").strip().lower()
voting_provider_capability(normalized_id)
if normalized_id != self.provider_id:
raise ValueError("Voting provider assurance id must be normalized.")
for field_name in ("implementation_ref", "protocol_ref", "protocol_version"):
if not str(getattr(self, field_name) or "").strip():
raise ValueError(
f"Voting provider assurance {field_name} is required."
)
profiles = tuple(self.supported_assurance_profiles)
allowed_profiles = {
VOTING_ASSURANCE_CONFIDENTIAL,
VOTING_ASSURANCE_SECRET,
VOTING_ASSURANCE_EXTERNAL_CERTIFIED,
}
if (
not profiles
or len(set(profiles)) != len(profiles)
or not set(profiles) <= allowed_profiles
):
raise ValueError(
"Voting provider assurance profiles must be unique supported external profiles."
)
if self.certification_state not in {
VOTING_CERTIFICATION_NOT_CERTIFIED,
VOTING_CERTIFICATION_IN_EVALUATION,
VOTING_CERTIFICATION_CERTIFIED,
VOTING_CERTIFICATION_EXPIRED,
VOTING_CERTIFICATION_REVOKED,
}:
raise ValueError("Voting provider certification state is invalid.")
valid_from = _aware_datetime(
self.certification_valid_from,
field_name="certification_valid_from",
)
valid_until = _aware_datetime(
self.certification_valid_until,
field_name="certification_valid_until",
)
if valid_from and valid_until and valid_until <= valid_from:
raise ValueError(
"Voting provider certification validity must end after it starts."
)
if self.certification_state == VOTING_CERTIFICATION_CERTIFIED:
required = (
self.certification_authority,
self.certification_reference,
self.certification_evidence_ref,
valid_from,
valid_until,
)
if any(value is None or value == "" for value in required):
raise ValueError(
"Certified Voting providers require authority, reference, evidence, and a validity window."
)
if len(self.notes) > 16 or any(not str(item or "").strip() for item in self.notes):
raise ValueError("Voting provider assurance notes must be bounded non-empty text.")
def is_currently_certified(self, *, at: datetime | None = None) -> bool:
if self.certification_state != VOTING_CERTIFICATION_CERTIFIED:
return False
moment = _aware_datetime(at or datetime.now(UTC), field_name="at")
valid_from = _aware_datetime(
self.certification_valid_from,
field_name="certification_valid_from",
)
valid_until = _aware_datetime(
self.certification_valid_until,
field_name="certification_valid_until",
)
return bool(valid_from and valid_until and valid_from <= moment < valid_until)
def to_dict(self) -> dict[str, object]:
return {
"provider_id": self.provider_id,
"implementation_ref": self.implementation_ref,
"supported_assurance_profiles": list(self.supported_assurance_profiles),
"certification_state": self.certification_state,
"protocol_ref": self.protocol_ref,
"protocol_version": self.protocol_version,
"certification_authority": self.certification_authority,
"certification_reference": self.certification_reference,
"certification_evidence_ref": self.certification_evidence_ref,
"certification_valid_from": _datetime_text(
self.certification_valid_from
),
"certification_valid_until": _datetime_text(
self.certification_valid_until
),
"notes": list(self.notes),
}
@dataclass(frozen=True, slots=True)
class VotingOption:
key: str
@@ -184,6 +311,8 @@ class ExternalVotingProvider(Protocol):
provider credentials must not cross this boundary.
"""
def assurance_declaration(self) -> VotingProviderAssuranceDeclaration: ...
def finalize_ballot(
self,
session: object,
@@ -283,6 +412,45 @@ class VotingBallotProvider(Protocol):
) -> VotingBallotRef: ...
def require_voting_provider_assurance(
provider: object,
*,
provider_id: str,
assurance_profile: str,
at: datetime | None = None,
) -> VotingProviderAssuranceDeclaration:
"""Validate and return the provider claim required for a frozen ballot."""
if not isinstance(provider, ExternalVotingProvider):
raise VotingCapabilityError("Voting provider does not implement the contract.")
try:
declaration = provider.assurance_declaration()
except (TypeError, ValueError) as exc:
raise VotingCapabilityError(
"Voting provider assurance declaration was rejected."
) from exc
if not isinstance(declaration, VotingProviderAssuranceDeclaration):
raise VotingCapabilityError(
"Voting provider returned an invalid assurance declaration."
)
normalized_provider_id = str(provider_id or "").strip().lower()
if declaration.provider_id != normalized_provider_id:
raise VotingCapabilityError(
"Voting provider assurance declaration does not match the selected provider."
)
if assurance_profile not in declaration.supported_assurance_profiles:
raise VotingCapabilityError(
"Voting provider does not support the selected assurance profile."
)
if (
assurance_profile == VOTING_ASSURANCE_EXTERNAL_CERTIFIED
and not declaration.is_currently_certified(at=at)
):
raise VotingCapabilityError(
"Externally certified Voting requires a currently valid provider certification."
)
return declaration
def _validate_provider_evidence(
evidence: Sequence[Mapping[str, object]],
) -> None:
@@ -330,6 +498,25 @@ def _reject_sensitive_evidence(value: object) -> None:
_reject_sensitive_evidence(nested)
def _aware_datetime(
value: datetime | None,
*,
field_name: str,
) -> datetime | None:
if value is None:
return None
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError(
f"Voting provider assurance {field_name} must be timezone-aware."
)
return value.astimezone(UTC)
def _datetime_text(value: datetime | None) -> str | None:
aware = _aware_datetime(value, field_name="datetime")
return aware.isoformat() if aware is not None else None
__all__ = [
"CAPABILITY_VOTING_BALLOTS",
"CAPABILITY_VOTING_PROVIDER_PREFIX",
@@ -343,6 +530,11 @@ __all__ = [
"VOTING_ASSURANCE_EXTERNAL_CERTIFIED",
"VOTING_ASSURANCE_RECORDED",
"VOTING_ASSURANCE_SECRET",
"VOTING_CERTIFICATION_CERTIFIED",
"VOTING_CERTIFICATION_EXPIRED",
"VOTING_CERTIFICATION_IN_EVALUATION",
"VOTING_CERTIFICATION_NOT_CERTIFIED",
"VOTING_CERTIFICATION_REVOKED",
"VotingBallotCreateCommand",
"VotingBallotProvider",
"VotingBallotRef",
@@ -350,7 +542,10 @@ __all__ = [
"VotingCastCommand",
"VotingElector",
"VotingOption",
"VotingProviderAssuranceDeclaration",
"VotingProviderCertificationState",
"VotingReceipt",
"VotingResult",
"require_voting_provider_assurance",
"voting_provider_capability",
]