Establish certifiable Voting provider boundary

This commit is contained in:
2026-08-04 14:01:26 +02:00
parent 0168c5ecd5
commit 8cfd6bfd48
8 changed files with 371 additions and 2 deletions
@@ -21,8 +21,10 @@ from govoplan_core.core.voting import (
ExternalVotingCastRequest,
ExternalVotingFinalizationRequest,
ExternalVotingPreparationRequest,
VOTING_CERTIFICATION_NOT_CERTIFIED,
VotingReceipt,
VotingResult,
VotingProviderAssuranceDeclaration,
)
from govoplan_voting.backend.db.models import (
VotingConfidentialBallot,
@@ -49,6 +51,19 @@ class LocalConfidentialVotingProvider:
def __init__(self, registry: object | None) -> None:
self._registry = registry
def assurance_declaration(self) -> VotingProviderAssuranceDeclaration:
return VotingProviderAssuranceDeclaration(
provider_id=LOCAL_CONFIDENTIAL_PROVIDER_ID,
implementation_ref="govoplan-voting/local-confidential@1",
supported_assurance_profiles=("confidential",),
certification_state=VOTING_CERTIFICATION_NOT_CERTIFIED,
protocol_ref="govoplan:voting:local-confidential",
protocol_version="1.0",
notes=(
"Server-readable reference provider; no anonymity, secrecy, coercion-resistance, or certification claim.",
),
)
def prepare_ballot(
self,
session: object,
+17 -1
View File
@@ -296,6 +296,7 @@ manifest = ModuleManifest(
body=(
"Opening a ballot freezes its definition and electorate hashes. Native recorded ballots retain active vote records for reconstruction; they are not secret. "
"Confidential, secret, and externally certified profiles require an installed provider and retain only aggregate results, receipts, hashes, and evidence. "
"Provider assurance, protocol identity, certificate evidence, and validity are pinned when the ballot opens and revalidated before provider effects. "
"The bundled local confidential provider encrypts raw selections through Encryption and supports interactive casting, but remains server-readable and uncertified. "
"Closure, certification, challenge, and annulment remain separate auditable transitions."
),
@@ -308,6 +309,16 @@ manifest = ModuleManifest(
href="govoplan-voting/docs/VOTING_DOMAIN.md",
kind="repository",
),
DocumentationLink(
label="POLYAS provider profile",
href="govoplan-voting/docs/POLYAS_PROVIDER_PROFILE.md",
kind="repository",
),
DocumentationLink(
label="Native certifiable Voting program",
href="govoplan-voting/docs/CERTIFIABLE_VOTING_PROGRAM.md",
kind="repository",
),
),
metadata={
"seed": True,
@@ -382,6 +393,7 @@ manifest = ModuleManifest(
"The native profile is recorded and reconstructable, not cryptographically secret.",
"Confidential, secret, and externally certified profiles require an installed provider capability and fail closed otherwise.",
"The bundled local confidential provider is server-decryptable and is neither anonymous, coercion-resistant, secret, nor externally certified.",
"POLYAS remains an operator-assisted integration target until a contracted API, sandbox, current certification evidence, and conformance fixtures are available.",
"Formal public-election certification remains a deployment-specific legal, organizational, and provider assurance decision.",
),
supported_authority_modes=("native_authoritative", "external_authoritative"),
@@ -402,7 +414,11 @@ manifest = ModuleManifest(
reference_packages=("product.service-to-decision",),
migration_docs=("docs/VOTING_DOMAIN.md",),
recovery_docs=("docs/VOTING_DOMAIN.md",),
security_docs=("docs/VOTING_DOMAIN.md",),
security_docs=(
"docs/VOTING_DOMAIN.md",
"docs/POLYAS_PROVIDER_PROFILE.md",
"docs/CERTIFIABLE_VOTING_PROGRAM.md",
),
operations_docs=("docs/VOTING_DOMAIN.md",),
),
)
+38
View File
@@ -21,8 +21,10 @@ from govoplan_core.core.voting import (
VotingBallotCreateCommand,
VotingBallotRef,
VotingCastCommand,
VotingCapabilityError,
VotingReceipt,
VotingResult,
require_voting_provider_assurance,
voting_provider_capability,
)
from govoplan_voting.backend.db.models import (
@@ -265,6 +267,16 @@ class SqlVotingBallots:
raise VotingStoreError(
"The selected Voting assurance profile requires an available external provider."
)
try:
declaration = require_voting_provider_assurance(
provider,
provider_id=provider_id,
assurance_profile=profile,
at=_now(),
)
except VotingCapabilityError as exc:
raise VotingStoreError(str(exc)) from exc
payload["provider_assurance"] = declaration.to_dict()
definition_hash, electorate_hash = _frozen_hashes(payload)
payload["definition_sha256"] = definition_hash
payload["electorate_sha256"] = electorate_hash
@@ -343,6 +355,7 @@ class SqlVotingBallots:
"electorate_sha256": electorate_hash,
"provider_id": payload.get("provider_id"),
"provider_ballot_ref": payload.get("provider_ballot_ref"),
"provider_assurance": payload.get("provider_assurance"),
"provider_evidence": [dict(item) for item in provider_evidence],
},
)
@@ -403,6 +416,7 @@ class SqlVotingBallots:
raise VotingStoreError(
"This Voting provider does not expose an interactive cast capability."
)
_require_pinned_provider_assurance(current.payload, provider)
try:
receipt = provider.cast_ballot(
typed_session,
@@ -863,6 +877,7 @@ class SqlVotingBallots:
provider = _capability(self._registry, voting_provider_capability(provider_id))
if not isinstance(provider, ExternalVotingProvider):
raise VotingStoreError(f"Voting provider is unavailable: {provider_id}.")
_require_pinned_provider_assurance(current.payload, provider)
electorate = list(current.payload["electorate"])
try:
result = provider.finalize_ballot(
@@ -944,6 +959,7 @@ def _payload_from_command(command: VotingBallotCreateCommand) -> dict[str, Any]:
"closes_at": _datetime_text(command.closes_at),
"provider_id": _optional_text(command.provider_id),
"provider_ballot_ref": _optional_text(command.provider_ballot_ref),
"provider_assurance": None,
"metadata": dict(command.metadata),
"definition_sha256": None,
"electorate_sha256": None,
@@ -1029,6 +1045,28 @@ def _validate_window(payload: Mapping[str, Any]) -> None:
raise VotingStoreError("Voting ballot has already reached its close time.")
def _require_pinned_provider_assurance(
payload: Mapping[str, Any],
provider: object,
) -> None:
provider_id = str(payload.get("provider_id") or "").strip()
assurance_profile = str(payload.get("assurance_profile") or "").strip()
try:
current = require_voting_provider_assurance(
provider,
provider_id=provider_id,
assurance_profile=assurance_profile,
at=_now(),
)
except VotingCapabilityError as exc:
raise VotingStoreError(str(exc)) from exc
pinned = payload.get("provider_assurance")
if not isinstance(pinned, Mapping) or dict(pinned) != current.to_dict():
raise VotingStoreError(
"Voting provider assurance changed after the ballot was frozen."
)
def _frozen_hashes(payload: Mapping[str, Any]) -> tuple[str, str]:
electorate = list(payload.get("electorate") or [])
definition = {