Implement governed encryption lifecycle

This commit is contained in:
2026-08-01 20:57:27 +02:00
parent c78deab5b5
commit 858c41d5ad
14 changed files with 3667 additions and 111 deletions
+186 -17
View File
@@ -1,13 +1,33 @@
from __future__ import annotations
from pathlib import Path
from govoplan_core.core.encryption import (
CAPABILITY_ENCRYPTION_CONTENT_PROTECTION,
CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT,
CAPABILITY_ENCRYPTION_KEY_VAULT,
CAPABILITY_ENCRYPTION_RECOVERY,
)
from govoplan_core.core.module_guards import (
drop_table_retirement_provider,
persistent_table_uninstall_guard,
)
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationLink,
DocumentationTopic,
MigrationSpec,
ModuleContext,
ModuleInterfaceProvider,
ModuleManifest,
ModuleUninstallGuardResult,
PermissionDefinition,
RoleTemplate,
)
from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.db.base import Base
from govoplan_encryption.backend.db import models
from govoplan_encryption.backend.service import SqlEncryptionService
MODULE_ID = "encryption"
@@ -27,14 +47,11 @@ OPTIONAL_DEPENDENCIES = (
"postbox",
"campaigns",
"workflow_engine",
"identity_trust",
)
def _permission(
scope: str,
label: str,
description: str,
) -> PermissionDefinition:
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2)
return PermissionDefinition(
scope=scope,
@@ -48,21 +65,64 @@ def _permission(
)
def _router(context: ModuleContext):
from govoplan_encryption.backend.router import create_router
return create_router(context.registry)
def _service(context: ModuleContext) -> SqlEncryptionService:
return SqlEncryptionService(context.registry)
def _disable_guard(
session: object | None,
_module_id: str,
) -> tuple[ModuleUninstallGuardResult, ...]:
if session is None:
return (
ModuleUninstallGuardResult(
"blocker",
"encryption_disable_unverified",
"Encryption cannot be disabled without proving the state of every protection envelope.",
),
)
try:
report = SqlEncryptionService().assess_disable(session)
except Exception as exc:
return (
ModuleUninstallGuardResult(
"blocker",
"encryption_disable_check_failed",
f"Encryption disable preflight failed: {type(exc).__name__}.",
),
)
if report.allowed:
return ()
return (
ModuleUninstallGuardResult(
"blocker",
"encryption_protected_content_present",
f"Encryption still protects {report.unresolved_count} unresolved envelope(s). Migrate, decrypt, explicitly export, or cryptographically destroy them before disabling the module.",
),
)
PERMISSIONS = (
_permission(
USE_SCOPE,
"Use encryption profiles",
"Protect and decrypt authorized content through an available profile.",
"Register and resolve protected content through configured profiles.",
),
_permission(
ADMIN_SCOPE,
"Administer encryption",
"Manage vaults, protection profiles, key rotation, and provider policy.",
"Manage vault metadata, provider operations, rotation, migration, and policy provenance.",
),
_permission(
RECOVERY_SCOPE,
"Approve key recovery",
"Participate in an auditable recovery ceremony without gaining content ownership.",
"Participate in a high-assurance recovery ceremony without gaining content ownership.",
),
)
@@ -87,7 +147,10 @@ manifest = ModuleManifest(
version=MODULE_VERSION,
optional_dependencies=OPTIONAL_DEPENDENCIES,
provides_interfaces=(
ModuleInterfaceProvider(name="encryption.key_vault", version="1.0.0"),
ModuleInterfaceProvider(
name="encryption.key_vault",
version="1.0.0",
),
ModuleInterfaceProvider(
name="encryption.content_protection",
version="1.0.0",
@@ -103,6 +166,81 @@ manifest = ModuleManifest(
),
permissions=PERMISSIONS,
role_templates=ROLE_TEMPLATES,
route_factory=_router,
capability_factories={
CAPABILITY_ENCRYPTION_KEY_VAULT: _service,
CAPABILITY_ENCRYPTION_CONTENT_PROTECTION: _service,
CAPABILITY_ENCRYPTION_RECOVERY: _service,
CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT: _service,
},
capability_documentation={
CAPABILITY_ENCRYPTION_KEY_VAULT: CapabilityDocumentation(
label="Governed encryption key vault",
summary=(
"Orchestrates opaque, idempotent provider key references, "
"lifecycle state, and policy provenance without exposing key material."
),
contract_version="1.0.0",
audience=("administrator", "security_officer", "auditor"),
),
CAPABILITY_ENCRYPTION_CONTENT_PROTECTION: CapabilityDocumentation(
label="Content-protection envelope registry",
summary=(
"Registers versioned protection envelopes and fail-closed, "
"evidence-backed migration state for feature-owned content."
),
contract_version="1.0.0",
),
CAPABILITY_ENCRYPTION_RECOVERY: CapabilityDocumentation(
label="Encryption recovery ceremony",
summary=(
"Requires recent high assurance, distinct custodians, quorum, "
"expiry, and immutable evidence without changing ownership."
),
contract_version="1.0.0",
),
CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT: CapabilityDocumentation(
label="Encryption disable preflight",
summary=(
"Blocks disable or uninstall while any protection envelope "
"remains unresolved."
),
contract_version="1.0.0",
),
},
migration_spec=MigrationSpec(
module_id=MODULE_ID,
metadata=Base.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
retirement_supported=True,
retirement_provider=drop_table_retirement_provider(
models.RecoveryApproval,
models.RecoveryCeremony,
models.ProtectionMigration,
models.ContentProtectionRecord,
models.EncryptionKeyOperation,
models.EncryptionKeyVersion,
models.EncryptionVault,
label=MODULE_NAME,
),
retirement_notes=(
"Destructive retirement remains blocked until disable preflight "
"proves that no unresolved protected envelope remains."
),
),
uninstall_guard_providers=(
_disable_guard,
persistent_table_uninstall_guard(
models.EncryptionVault,
models.EncryptionKeyVersion,
models.EncryptionKeyOperation,
models.ContentProtectionRecord,
models.ProtectionMigration,
models.RecoveryCeremony,
models.RecoveryApproval,
label=MODULE_NAME,
),
),
documentation=(
DocumentationTopic(
id="encryption.boundary",
@@ -114,26 +252,57 @@ manifest = ModuleManifest(
body=(
"Encryption protects feature-owned content without taking over "
"its business ownership. Resource ownership recovery never "
"implicitly grants cryptographic keys. Disabling the module is "
"blocked until protected objects are decrypted, rewrapped, "
"explicitly exported, or cryptographically deleted."
"implicitly grants cryptographic keys. High-risk lifecycle "
"actions require recent Identity Trust assurance. Disabling is "
"blocked until each envelope is migrated, decrypted, explicitly "
"exported, or cryptographically destroyed. No bundled provider "
"or E2EE claim is implied by enabling this module."
),
layer="available",
documentation_types=("admin", "user"),
audience=("user", "administrator", "security_officer", "product_owner"),
audience=(
"user",
"administrator",
"security_officer",
"product_owner",
"auditor",
),
related_modules=OPTIONAL_DEPENDENCIES,
order=100,
links=(
DocumentationLink(
label="Encryption boundary and threat model",
href="govoplan-encryption/docs/ENCRYPTION_BOUNDARY.md",
kind="repository",
),
),
),
),
architecture=declared_module_architecture(
layer="institutional_foundation",
kind="foundation",
maturity="scaffold",
maturity="vertical_slice",
documentation_ref="docs/ENCRYPTION_BOUNDARY.md",
known_limits=("No production key vault, protected-content persistence, rotation worker, or recovery ceremony is implemented yet.",),
owned_concepts=("key vault", "content-protection envelope", "cryptographic recovery ceremony"),
non_owned_concepts=("domain content", "resource ownership", "account authentication"),
test_ref="tests/test_encryption.py",
known_limits=(
"The module orchestrates references and evidence but ships no concrete cryptographic provider, raw key store, cipher implementation, client E2EE protocol, KMS/HSM conformance suite, or production recovery executor.",
"A true E2EE claim remains prohibited until a selected client/provider profile passes its threat model, interoperability fixtures, backup/restore tests, and independent review.",
),
owned_concepts=(
"key vault",
"content-protection envelope",
"cryptographic recovery ceremony",
),
non_owned_concepts=(
"domain content",
"resource ownership",
"account authentication",
"provider key material",
),
migration_docs=("docs/ENCRYPTION_BOUNDARY.md",),
recovery_docs=("docs/ENCRYPTION_BOUNDARY.md",),
security_docs=("docs/ENCRYPTION_BOUNDARY.md",),
operations_docs=("README.md",),
),
)