Establish certifiable Voting provider boundary
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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",),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user