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
@@ -0,0 +1,19 @@
from govoplan_encryption.backend.db.models import (
ContentProtectionRecord,
EncryptionKeyOperation,
EncryptionKeyVersion,
EncryptionVault,
ProtectionMigration,
RecoveryApproval,
RecoveryCeremony,
)
__all__ = [
"ContentProtectionRecord",
"EncryptionKeyOperation",
"EncryptionKeyVersion",
"EncryptionVault",
"ProtectionMigration",
"RecoveryApproval",
"RecoveryCeremony",
]
@@ -0,0 +1,313 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
import uuid
from sqlalchemy import (
DateTime,
Index,
Integer,
JSON,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from govoplan_core.db.base import Base, TimestampMixin
def new_uuid() -> str:
return str(uuid.uuid4())
class EncryptionVault(Base, TimestampMixin):
__tablename__ = "encryption_vaults"
__table_args__ = (
UniqueConstraint("tenant_id", "vault_id", name="uq_encryption_vault"),
UniqueConstraint(
"tenant_id",
"create_idempotency_key",
name="uq_encryption_vault_create_idempotency",
),
Index(
"ix_encryption_vault_scope",
"tenant_id",
"scope_type",
"scope_id",
"state",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
vault_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
provider_id: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
purpose: Mapped[str] = mapped_column(String(255), nullable=False)
profile_kind: Mapped[str] = mapped_column(String(40), nullable=False)
scope_type: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
scope_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
policy_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
recovery_quorum: Mapped[int] = mapped_column(Integer, nullable=False)
state: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
current_key_version: Mapped[int | None] = mapped_column(Integer, nullable=True)
create_idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
create_request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
provenance: Mapped[dict[str, Any]] = mapped_column(
JSON, default=dict, nullable=False
)
created_by: Mapped[str] = mapped_column(String(255), nullable=False)
updated_by: Mapped[str] = mapped_column(String(255), nullable=False)
class EncryptionKeyVersion(Base, TimestampMixin):
__tablename__ = "encryption_key_versions"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"vault_id",
"version",
name="uq_encryption_key_version",
),
UniqueConstraint(
"tenant_id",
"provider_id",
"provider_key_ref",
name="uq_encryption_provider_key_ref",
),
Index(
"ix_encryption_key_vault_state",
"tenant_id",
"vault_id",
"state",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
vault_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
version: Mapped[int] = mapped_column(Integer, nullable=False)
provider_id: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
provider_key_ref: Mapped[str | None] = mapped_column(String(1000), nullable=True)
algorithm_suite: Mapped[str] = mapped_column(String(120), nullable=False)
state: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
public_key_ref: Mapped[str | None] = mapped_column(String(1000), nullable=True)
imported: Mapped[bool] = mapped_column(default=False, nullable=False)
exportable: Mapped[bool] = mapped_column(default=False, nullable=False)
provider_version: Mapped[str | None] = mapped_column(String(120), nullable=True)
provider_provenance: Mapped[dict[str, Any]] = mapped_column(
JSON, default=dict, nullable=False
)
activated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
revoked_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
destruction_scheduled_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
destroyed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class EncryptionKeyOperation(Base, TimestampMixin):
__tablename__ = "encryption_key_operations"
__table_args__ = (
UniqueConstraint(
"tenant_id", "idempotency_key", name="uq_encryption_key_operation_idem"
),
Index(
"ix_encryption_key_operation_state",
"tenant_id",
"state",
"updated_at",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
vault_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
key_version: Mapped[int] = mapped_column(Integer, nullable=False)
operation: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
provider_id: Mapped[str] = mapped_column(String(120), nullable=False)
state: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
request_payload: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
error_code: Mapped[str | None] = mapped_column(String(255), nullable=True)
policy_decision_ref: Mapped[str | None] = mapped_column(String(1000), nullable=True)
assurance_evidence_ref: Mapped[str | None] = mapped_column(
String(1000), nullable=True
)
requested_by: Mapped[str] = mapped_column(String(255), nullable=False)
completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class ContentProtectionRecord(Base, TimestampMixin):
__tablename__ = "encryption_content_protections"
__table_args__ = (
UniqueConstraint(
"tenant_id", "envelope_id", name="uq_encryption_content_envelope"
),
UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_content_idempotency",
),
Index(
"ix_encryption_content_owner",
"tenant_id",
"owner_module",
"resource_type",
"resource_id",
),
Index("ix_encryption_content_state", "tenant_id", "state", "updated_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
envelope_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
owner_module: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
resource_type: Mapped[str] = mapped_column(String(120), nullable=False)
resource_id: Mapped[str] = mapped_column(String(255), nullable=False)
profile_kind: Mapped[str] = mapped_column(String(40), nullable=False)
profile_id: Mapped[str] = mapped_column(String(255), nullable=False)
provider_id: Mapped[str] = mapped_column(String(120), nullable=False)
vault_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
key_version: Mapped[int] = mapped_column(Integer, nullable=False)
algorithm_suite: Mapped[str] = mapped_column(String(120), nullable=False)
ciphertext_ref: Mapped[str] = mapped_column(String(2000), nullable=False)
ciphertext_digest: Mapped[str] = mapped_column(String(255), nullable=False)
authenticated_context_digest: Mapped[str] = mapped_column(
String(255), nullable=False
)
wrapped_key_refs: Mapped[list[str]] = mapped_column(
JSON, default=list, nullable=False
)
state: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
source_envelope_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
migration_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
envelope_metadata: Mapped[dict[str, Any]] = mapped_column(
JSON, default=dict, nullable=False
)
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
policy_decision_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
registered_by: Mapped[str] = mapped_column(String(255), nullable=False)
class ProtectionMigration(Base, TimestampMixin):
__tablename__ = "encryption_protection_migrations"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_migration_idempotency",
),
Index("ix_encryption_migration_state", "tenant_id", "state", "updated_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
source_envelope_id: Mapped[str] = mapped_column(
String(255), nullable=False, index=True
)
target_envelope_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
target_provider_id: Mapped[str] = mapped_column(String(120), nullable=False)
target_vault_id: Mapped[str] = mapped_column(String(255), nullable=False)
target_key_version: Mapped[int] = mapped_column(Integer, nullable=False)
target_algorithm_suite: Mapped[str] = mapped_column(String(120), nullable=False)
mode: Mapped[str] = mapped_column(String(40), nullable=False)
state: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
policy_decision_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
assurance_evidence_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
evidence_refs: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
error_code: Mapped[str | None] = mapped_column(String(255), nullable=True)
provenance: Mapped[dict[str, Any]] = mapped_column(
JSON, default=dict, nullable=False
)
requested_by: Mapped[str] = mapped_column(String(255), nullable=False)
completed_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
class RecoveryCeremony(Base, TimestampMixin):
__tablename__ = "encryption_recovery_ceremonies"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_recovery_idempotency",
),
Index("ix_encryption_recovery_state", "tenant_id", "state", "expires_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
vault_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
state: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
requested_scope: Mapped[str] = mapped_column(String(255), nullable=False)
reason: Mapped[str] = mapped_column(Text, nullable=False)
quorum: Mapped[int] = mapped_column(Integer, nullable=False)
revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
policy_decision_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
requester_assurance_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
requester_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, index=True
)
execution_ref: Mapped[str | None] = mapped_column(String(1000), nullable=True)
provenance: Mapped[dict[str, Any]] = mapped_column(
JSON, default=dict, nullable=False
)
class RecoveryApproval(Base, TimestampMixin):
__tablename__ = "encryption_recovery_approvals"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"recovery_id",
"approver_account_id",
name="uq_encryption_recovery_approver",
),
UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_recovery_approval_idem",
),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
recovery_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
approver_account_id: Mapped[str] = mapped_column(String(255), nullable=False)
decision: Mapped[str] = mapped_column(String(20), nullable=False)
reason: Mapped[str] = mapped_column(Text, nullable=False)
assurance_evidence_ref: Mapped[str] = mapped_column(String(1000), nullable=False)
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
request_digest: Mapped[str] = mapped_column(String(64), nullable=False)
__all__ = [
"ContentProtectionRecord",
"EncryptionKeyOperation",
"EncryptionKeyVersion",
"EncryptionVault",
"ProtectionMigration",
"RecoveryApproval",
"RecoveryCeremony",
"new_uuid",
]
+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",),
),
)
@@ -0,0 +1 @@
"""Encryption database migrations."""
@@ -0,0 +1 @@
"""Encryption Alembic revisions."""
@@ -0,0 +1,319 @@
"""v0.1.14 encryption lifecycle and protection metadata
Revision ID: d4a6b8c0e2f3
Revises: None
Create Date: 2026-08-01 00:00:00.000000
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = "d4a6b8c0e2f3"
down_revision = None
branch_labels = None
depends_on = None
def _timestamps() -> tuple[sa.Column, sa.Column]:
return (
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)
def upgrade() -> None:
op.create_table(
"encryption_vaults",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("vault_id", sa.String(255), nullable=False),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("provider_id", sa.String(120), nullable=False),
sa.Column("purpose", sa.String(255), nullable=False),
sa.Column("profile_kind", sa.String(40), nullable=False),
sa.Column("scope_type", sa.String(80), nullable=False),
sa.Column("scope_id", sa.String(255), nullable=True),
sa.Column("policy_ref", sa.String(1000), nullable=False),
sa.Column("recovery_quorum", sa.Integer(), nullable=False),
sa.Column("state", sa.String(40), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("current_key_version", sa.Integer(), nullable=True),
sa.Column("create_idempotency_key", sa.String(255), nullable=False),
sa.Column("create_request_digest", sa.String(64), nullable=False),
sa.Column("provenance", sa.JSON(), nullable=False),
sa.Column("created_by", sa.String(255), nullable=False),
sa.Column("updated_by", sa.String(255), nullable=False),
*_timestamps(),
sa.UniqueConstraint("tenant_id", "vault_id", name="uq_encryption_vault"),
sa.UniqueConstraint(
"tenant_id",
"create_idempotency_key",
name="uq_encryption_vault_create_idempotency",
),
)
op.create_index(
"ix_encryption_vault_scope",
"encryption_vaults",
["tenant_id", "scope_type", "scope_id", "state"],
)
for column in (
"tenant_id",
"vault_id",
"provider_id",
"scope_type",
"scope_id",
"state",
):
op.create_index(f"ix_encryption_vaults_{column}", "encryption_vaults", [column])
op.create_table(
"encryption_key_versions",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("vault_id", sa.String(255), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("provider_id", sa.String(120), nullable=False),
sa.Column("provider_key_ref", sa.String(1000), nullable=True),
sa.Column("algorithm_suite", sa.String(120), nullable=False),
sa.Column("state", sa.String(40), nullable=False),
sa.Column("public_key_ref", sa.String(1000), nullable=True),
sa.Column("imported", sa.Boolean(), nullable=False),
sa.Column("exportable", sa.Boolean(), nullable=False),
sa.Column("provider_version", sa.String(120), nullable=True),
sa.Column("provider_provenance", sa.JSON(), nullable=False),
sa.Column("activated_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.Column(
"destruction_scheduled_at", sa.DateTime(timezone=True), nullable=True
),
sa.Column("destroyed_at", sa.DateTime(timezone=True), nullable=True),
*_timestamps(),
sa.UniqueConstraint(
"tenant_id", "vault_id", "version", name="uq_encryption_key_version"
),
sa.UniqueConstraint(
"tenant_id",
"provider_id",
"provider_key_ref",
name="uq_encryption_provider_key_ref",
),
)
op.create_index(
"ix_encryption_key_vault_state",
"encryption_key_versions",
["tenant_id", "vault_id", "state"],
)
for column in ("tenant_id", "vault_id", "provider_id", "state"):
op.create_index(
f"ix_encryption_key_versions_{column}",
"encryption_key_versions",
[column],
)
op.create_table(
"encryption_key_operations",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("vault_id", sa.String(255), nullable=False),
sa.Column("key_version", sa.Integer(), nullable=False),
sa.Column("operation", sa.String(40), nullable=False),
sa.Column("provider_id", sa.String(120), nullable=False),
sa.Column("state", sa.String(40), nullable=False),
sa.Column("idempotency_key", sa.String(255), nullable=False),
sa.Column("request_digest", sa.String(64), nullable=False),
sa.Column("request_payload", sa.JSON(), nullable=False),
sa.Column("error_code", sa.String(255), nullable=True),
sa.Column("policy_decision_ref", sa.String(1000), nullable=True),
sa.Column("assurance_evidence_ref", sa.String(1000), nullable=True),
sa.Column("requested_by", sa.String(255), nullable=False),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
*_timestamps(),
sa.UniqueConstraint(
"tenant_id", "idempotency_key", name="uq_encryption_key_operation_idem"
),
)
op.create_index(
"ix_encryption_key_operation_state",
"encryption_key_operations",
["tenant_id", "state", "updated_at"],
)
for column in ("tenant_id", "vault_id", "operation", "state"):
op.create_index(
f"ix_encryption_key_operations_{column}",
"encryption_key_operations",
[column],
)
op.create_table(
"encryption_content_protections",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("envelope_id", sa.String(255), nullable=False),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("owner_module", sa.String(120), nullable=False),
sa.Column("resource_type", sa.String(120), nullable=False),
sa.Column("resource_id", sa.String(255), nullable=False),
sa.Column("profile_kind", sa.String(40), nullable=False),
sa.Column("profile_id", sa.String(255), nullable=False),
sa.Column("provider_id", sa.String(120), nullable=False),
sa.Column("vault_id", sa.String(255), nullable=False),
sa.Column("key_version", sa.Integer(), nullable=False),
sa.Column("algorithm_suite", sa.String(120), nullable=False),
sa.Column("ciphertext_ref", sa.String(2000), nullable=False),
sa.Column("ciphertext_digest", sa.String(255), nullable=False),
sa.Column("authenticated_context_digest", sa.String(255), nullable=False),
sa.Column("wrapped_key_refs", sa.JSON(), nullable=False),
sa.Column("state", sa.String(40), nullable=False),
sa.Column("source_envelope_id", sa.String(255), nullable=True),
sa.Column("migration_id", sa.String(36), nullable=True),
sa.Column("envelope_metadata", sa.JSON(), nullable=False),
sa.Column("idempotency_key", sa.String(255), nullable=False),
sa.Column("request_digest", sa.String(64), nullable=False),
sa.Column("policy_decision_ref", sa.String(1000), nullable=False),
sa.Column("registered_by", sa.String(255), nullable=False),
*_timestamps(),
sa.UniqueConstraint(
"tenant_id", "envelope_id", name="uq_encryption_content_envelope"
),
sa.UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_content_idempotency",
),
)
op.create_index(
"ix_encryption_content_owner",
"encryption_content_protections",
["tenant_id", "owner_module", "resource_type", "resource_id"],
)
op.create_index(
"ix_encryption_content_state",
"encryption_content_protections",
["tenant_id", "state", "updated_at"],
)
for column in ("envelope_id", "tenant_id", "owner_module", "vault_id", "state"):
op.create_index(
f"ix_encryption_content_protections_{column}",
"encryption_content_protections",
[column],
)
op.create_table(
"encryption_protection_migrations",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("source_envelope_id", sa.String(255), nullable=False),
sa.Column("target_envelope_id", sa.String(255), nullable=True),
sa.Column("target_provider_id", sa.String(120), nullable=False),
sa.Column("target_vault_id", sa.String(255), nullable=False),
sa.Column("target_key_version", sa.Integer(), nullable=False),
sa.Column("target_algorithm_suite", sa.String(120), nullable=False),
sa.Column("mode", sa.String(40), nullable=False),
sa.Column("state", sa.String(40), nullable=False),
sa.Column("policy_decision_ref", sa.String(1000), nullable=False),
sa.Column("assurance_evidence_ref", sa.String(1000), nullable=False),
sa.Column("idempotency_key", sa.String(255), nullable=False),
sa.Column("request_digest", sa.String(64), nullable=False),
sa.Column("evidence_refs", sa.JSON(), nullable=False),
sa.Column("error_code", sa.String(255), nullable=True),
sa.Column("provenance", sa.JSON(), nullable=False),
sa.Column("requested_by", sa.String(255), nullable=False),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
*_timestamps(),
sa.UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_migration_idempotency",
),
)
op.create_index(
"ix_encryption_migration_state",
"encryption_protection_migrations",
["tenant_id", "state", "updated_at"],
)
for column in ("tenant_id", "source_envelope_id", "state"):
op.create_index(
f"ix_encryption_protection_migrations_{column}",
"encryption_protection_migrations",
[column],
)
op.create_table(
"encryption_recovery_ceremonies",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("vault_id", sa.String(255), nullable=False),
sa.Column("state", sa.String(40), nullable=False),
sa.Column("requested_scope", sa.String(255), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("quorum", sa.Integer(), nullable=False),
sa.Column("revision", sa.Integer(), nullable=False),
sa.Column("policy_decision_ref", sa.String(1000), nullable=False),
sa.Column("requester_assurance_ref", sa.String(1000), nullable=False),
sa.Column("requester_account_id", sa.String(255), nullable=False),
sa.Column("idempotency_key", sa.String(255), nullable=False),
sa.Column("request_digest", sa.String(64), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("execution_ref", sa.String(1000), nullable=True),
sa.Column("provenance", sa.JSON(), nullable=False),
*_timestamps(),
sa.UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_recovery_idempotency",
),
)
op.create_index(
"ix_encryption_recovery_state",
"encryption_recovery_ceremonies",
["tenant_id", "state", "expires_at"],
)
for column in ("tenant_id", "vault_id", "state", "expires_at"):
op.create_index(
f"ix_encryption_recovery_ceremonies_{column}",
"encryption_recovery_ceremonies",
[column],
)
op.create_table(
"encryption_recovery_approvals",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("tenant_id", sa.String(36), nullable=False),
sa.Column("recovery_id", sa.String(36), nullable=False),
sa.Column("approver_account_id", sa.String(255), nullable=False),
sa.Column("decision", sa.String(20), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("assurance_evidence_ref", sa.String(1000), nullable=False),
sa.Column("idempotency_key", sa.String(255), nullable=False),
sa.Column("request_digest", sa.String(64), nullable=False),
*_timestamps(),
sa.UniqueConstraint(
"tenant_id",
"recovery_id",
"approver_account_id",
name="uq_encryption_recovery_approver",
),
sa.UniqueConstraint(
"tenant_id",
"idempotency_key",
name="uq_encryption_recovery_approval_idem",
),
)
for column in ("tenant_id", "recovery_id"):
op.create_index(
f"ix_encryption_recovery_approvals_{column}",
"encryption_recovery_approvals",
[column],
)
def downgrade() -> None:
op.drop_table("encryption_recovery_approvals")
op.drop_table("encryption_recovery_ceremonies")
op.drop_table("encryption_protection_migrations")
op.drop_table("encryption_content_protections")
op.drop_table("encryption_key_operations")
op.drop_table("encryption_key_versions")
op.drop_table("encryption_vaults")
+456
View File
@@ -0,0 +1,456 @@
from __future__ import annotations
from dataclasses import asdict
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from govoplan_core.audit.logging import audit_event
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.core.encryption import (
ContentProtectionEnvelope,
KeyLifecycleRequest,
KeyRotationRequest,
KeyVaultCreateRequest,
ProtectionMigrationRequest,
ProtectionRegistrationRequest,
RecoveryApprovalRequest,
RecoveryRequest,
)
from govoplan_core.db.session import get_session
from govoplan_encryption.backend.schemas import (
DisablePreflightResponse,
EnvelopePayload,
EnvelopeRegistrationPayload,
EnvelopeResponse,
KeyLifecyclePayload,
KeyRotationPayload,
MigrationOutcomePayload,
MigrationRequestPayload,
MigrationResponse,
RecoveryDecisionPayload,
RecoveryRequestPayload,
RecoveryResponse,
VaultCreatePayload,
VaultResponse,
)
from govoplan_encryption.backend.service import EncryptionError, SqlEncryptionService
def create_router(registry: object | None = None) -> APIRouter:
router = APIRouter(prefix="/encryption", tags=["encryption"])
service = SqlEncryptionService(registry)
@router.post("/vaults", response_model=VaultResponse)
def create_vault(
payload: VaultCreatePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> VaultResponse:
_require(principal, "encryption:vault:admin")
value = _call(
lambda: service.create_vault(
session,
principal,
request=KeyVaultCreateRequest(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
)
_audit(
session,
principal,
"encryption.vault.created",
"encryption_vault",
value.vault_id,
{"provider_id": value.provider_id, "state": value.state},
)
session.commit()
return _vault_response(value)
@router.get("/vaults/{vault_id}", response_model=VaultResponse)
def get_vault(
vault_id: str,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> VaultResponse:
_require(principal, "encryption:vault:use", "encryption:vault:admin")
value = service.get_vault(
session,
tenant_id=principal.tenant_id,
vault_id=vault_id,
)
if value is None:
raise HTTPException(status_code=404, detail="Encryption vault not found.")
return _vault_response(value)
@router.post("/vaults/{vault_id}/rotate", response_model=VaultResponse)
def rotate_key(
vault_id: str,
payload: KeyRotationPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> VaultResponse:
_require(principal, "encryption:vault:admin")
value = _call(
lambda: service.rotate_key(
session,
principal,
request=KeyRotationRequest(
tenant_id=principal.tenant_id,
vault_id=vault_id,
**payload.model_dump(),
),
)
)
_audit_lifecycle(session, principal, value, "rotated")
session.commit()
return _vault_response(value)
@router.post("/vaults/{vault_id}/revoke", response_model=VaultResponse)
def revoke_key(
vault_id: str,
payload: KeyLifecyclePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> VaultResponse:
_require(principal, "encryption:vault:admin")
value = _call(
lambda: service.revoke_key(
session,
principal,
request=KeyLifecycleRequest(
tenant_id=principal.tenant_id,
vault_id=vault_id,
**payload.model_dump(),
),
)
)
_audit_lifecycle(session, principal, value, "revoked")
session.commit()
return _vault_response(value)
@router.post("/vaults/{vault_id}/destruction", response_model=VaultResponse)
def schedule_destruction(
vault_id: str,
payload: KeyLifecyclePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> VaultResponse:
_require(principal, "encryption:vault:admin")
value = _call(
lambda: service.schedule_destruction(
session,
principal,
request=KeyLifecycleRequest(
tenant_id=principal.tenant_id,
vault_id=vault_id,
**payload.model_dump(),
),
)
)
_audit_lifecycle(session, principal, value, "destruction_scheduled")
session.commit()
return _vault_response(value)
@router.post("/vaults/{vault_id}/reconcile", response_model=VaultResponse)
def reconcile_vault(
vault_id: str,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> VaultResponse:
_require(principal, "encryption:vault:admin")
value = _call(
lambda: service.reconcile_vault(
session,
principal,
tenant_id=principal.tenant_id,
vault_id=vault_id,
)
)
_audit_lifecycle(session, principal, value, "reconciled")
session.commit()
return _vault_response(value)
@router.post("/envelopes", response_model=EnvelopeResponse)
def register_envelope(
payload: EnvelopeRegistrationPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> EnvelopeResponse:
_require(principal, "encryption:vault:use", "encryption:vault:admin")
envelope = _envelope_contract(payload.envelope, principal.tenant_id)
value = _call(
lambda: service.register_envelope(
session,
principal,
request=ProtectionRegistrationRequest(
envelope=envelope,
idempotency_key=payload.idempotency_key,
policy_decision_ref=payload.policy_decision_ref,
),
)
)
_audit(
session,
principal,
"encryption.envelope.registered",
"content_protection_envelope",
value.envelope_id,
{
"owner_module": value.owner_module,
"profile_kind": value.profile_kind,
"key_version": value.key_version,
},
)
session.commit()
return _envelope_response(value)
@router.get("/envelopes/{envelope_id}", response_model=EnvelopeResponse)
def get_envelope(
envelope_id: str,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> EnvelopeResponse:
_require(principal, "encryption:vault:use", "encryption:vault:admin")
value = service.get_envelope(
session,
principal,
tenant_id=principal.tenant_id,
envelope_id=envelope_id,
)
if value is None:
raise HTTPException(
status_code=404, detail="Protection envelope not found."
)
return _envelope_response(value)
@router.post("/migrations", response_model=MigrationResponse)
def request_migration(
payload: MigrationRequestPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> MigrationResponse:
_require(principal, "encryption:vault:admin")
value = _call(
lambda: service.request_migration(
session,
principal,
request=ProtectionMigrationRequest(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
)
_audit(
session,
principal,
"encryption.migration.requested",
"protection_migration",
value.migration_id,
{"state": value.state},
)
session.commit()
return _migration_response(value)
@router.post("/migrations/{migration_id}/outcome", response_model=MigrationResponse)
def record_migration_outcome(
migration_id: str,
payload: MigrationOutcomePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> MigrationResponse:
_require(principal, "encryption:vault:admin")
target = (
_envelope_contract(payload.target_envelope, principal.tenant_id)
if payload.target_envelope is not None
else None
)
value = _call(
lambda: service.record_migration_outcome(
session,
principal,
migration_id=migration_id,
state=payload.state,
evidence_refs=tuple(payload.evidence_refs),
target_envelope=target,
error_code=payload.error_code,
)
)
_audit(
session,
principal,
"encryption.migration.outcome_recorded",
"protection_migration",
migration_id,
{"state": value.state, "evidence_count": len(value.evidence_refs)},
)
session.commit()
return _migration_response(value)
@router.post("/recoveries", response_model=RecoveryResponse)
def request_recovery(
payload: RecoveryRequestPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> RecoveryResponse:
_require(principal, "encryption:recovery:approve")
value = _call(
lambda: service.request_recovery(
session,
principal,
request=RecoveryRequest(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
)
_audit(
session,
principal,
"encryption.recovery.requested",
"encryption_recovery",
value.recovery_id,
{"quorum": value.quorum, "requested_scope": value.requested_scope},
)
session.commit()
return RecoveryResponse(**_serializable(value))
@router.post("/recoveries/{recovery_id}/decision", response_model=RecoveryResponse)
def decide_recovery(
recovery_id: str,
payload: RecoveryDecisionPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> RecoveryResponse:
_require(principal, "encryption:recovery:approve")
value = _call(
lambda: service.decide_recovery(
session,
principal,
request=RecoveryApprovalRequest(
tenant_id=principal.tenant_id,
recovery_id=recovery_id,
**payload.model_dump(),
),
)
)
_audit(
session,
principal,
"encryption.recovery.decided",
"encryption_recovery",
recovery_id,
{"state": value.state, "approvals": value.approvals},
)
session.commit()
return RecoveryResponse(**_serializable(value))
@router.get("/disable-preflight", response_model=DisablePreflightResponse)
def disable_preflight(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> DisablePreflightResponse:
_require(principal, "encryption:vault:admin")
value = service.assess_disable(session, tenant_id=principal.tenant_id)
return DisablePreflightResponse(**_serializable(value))
return router
def _require(principal: ApiPrincipal, *scopes: str) -> None:
if any(has_scope(principal, scope) for scope in scopes):
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing one of: {', '.join(scopes)}",
)
def _call(callback):
try:
return callback()
except (EncryptionError, ValueError) as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
) from exc
def _envelope_contract(
payload: EnvelopePayload,
tenant_id: str,
) -> ContentProtectionEnvelope:
return ContentProtectionEnvelope(
tenant_id=tenant_id,
**payload.model_dump(),
)
def _serializable(value) -> dict[str, object]:
data = asdict(value)
for key in ("blocking_envelope_refs", "required_actions", "evidence_refs"):
if key in data and isinstance(data[key], tuple):
data[key] = list(data[key])
return data
def _vault_response(value) -> VaultResponse:
return VaultResponse(**_serializable(value))
def _envelope_response(value) -> EnvelopeResponse:
data = _serializable(value)
data["wrapped_key_refs"] = list(value.wrapped_key_refs)
return EnvelopeResponse(**data)
def _migration_response(value) -> MigrationResponse:
data = _serializable(value)
data["evidence_refs"] = list(value.evidence_refs)
if value.target_envelope is not None:
data["target_envelope"] = _envelope_response(value.target_envelope)
return MigrationResponse(**data)
def _audit_lifecycle(
session: Session,
principal: ApiPrincipal,
value,
action: str,
) -> None:
_audit(
session,
principal,
f"encryption.key.{action}",
"encryption_vault",
value.vault_id,
{
"state": value.state,
"revision": value.revision,
"key_version": value.current_key.version if value.current_key else None,
},
)
def _audit(
session: Session,
principal: ApiPrincipal,
action: str,
object_type: str,
object_id: str,
details: dict[str, object],
) -> None:
audit_event(
session,
tenant_id=principal.tenant_id,
user_id=getattr(principal.user, "id", None),
api_key_id=principal.api_key_id,
action=action,
object_type=object_type,
object_id=object_id,
details={**details, "secret_material_present": False},
)
__all__ = ["create_router"]
+220
View File
@@ -0,0 +1,220 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, Field
class VaultCreatePayload(BaseModel):
vault_id: str = Field(min_length=1, max_length=255)
name: str = Field(min_length=1, max_length=255)
provider_id: str = Field(min_length=1, max_length=120)
purpose: str = Field(min_length=1, max_length=255)
algorithm_suite: str = Field(min_length=1, max_length=120)
scope_type: str = Field(min_length=1, max_length=80)
scope_id: str | None = Field(default=None, max_length=255)
policy_ref: str = Field(min_length=1, max_length=1000)
idempotency_key: str = Field(min_length=1, max_length=255)
recovery_quorum: int = Field(default=2, ge=1, le=32)
profile_kind: Literal["server_envelope", "tenant_held", "end_to_end"] = (
"server_envelope"
)
import_reference: str | None = Field(default=None, max_length=1000)
provider_policy: dict[str, Any] = Field(default_factory=dict)
class KeyRotationPayload(BaseModel):
expected_revision: int = Field(ge=1)
reason: str = Field(min_length=1, max_length=2000)
policy_decision_ref: str = Field(min_length=1, max_length=1000)
assurance_evidence_ref: str = Field(min_length=1, max_length=1000)
idempotency_key: str = Field(min_length=1, max_length=255)
algorithm_suite: str | None = Field(default=None, max_length=120)
provider_policy: dict[str, Any] = Field(default_factory=dict)
class KeyLifecyclePayload(BaseModel):
key_version: int = Field(ge=1)
expected_revision: int = Field(ge=1)
reason: str = Field(min_length=1, max_length=2000)
policy_decision_ref: str = Field(min_length=1, max_length=1000)
assurance_evidence_ref: str = Field(min_length=1, max_length=1000)
idempotency_key: str = Field(min_length=1, max_length=255)
effective_at: datetime | None = None
class KeyVersionResponse(BaseModel):
tenant_id: str
vault_id: str
version: int
provider_id: str
provider_key_ref: str
algorithm_suite: str
state: str
created_at: datetime
activated_at: datetime | None = None
revoked_at: datetime | None = None
destruction_scheduled_at: datetime | None = None
destroyed_at: datetime | None = None
public_key_ref: str | None = None
imported: bool = False
exportable: bool = False
provenance: dict[str, Any] = Field(default_factory=dict)
contract_version: str = "1"
class VaultResponse(BaseModel):
tenant_id: str
vault_id: str
name: str
provider_id: str
purpose: str
profile_kind: str
scope_type: str
scope_id: str | None = None
policy_ref: str
recovery_quorum: int
state: str
revision: int
current_key: KeyVersionResponse | None = None
created_at: datetime
updated_at: datetime
provenance: dict[str, Any] = Field(default_factory=dict)
contract_version: str = "1"
class EnvelopePayload(BaseModel):
envelope_id: str = Field(min_length=1, max_length=255)
owner_module: str = Field(min_length=1, max_length=120)
resource_type: str = Field(min_length=1, max_length=120)
resource_id: str = Field(min_length=1, max_length=255)
profile_kind: Literal["server_envelope", "tenant_held", "end_to_end"]
profile_id: str = Field(min_length=1, max_length=255)
provider_id: str = Field(min_length=1, max_length=120)
vault_id: str = Field(min_length=1, max_length=255)
key_version: int = Field(ge=1)
algorithm_suite: str = Field(min_length=1, max_length=120)
ciphertext_ref: str = Field(min_length=1, max_length=2000)
ciphertext_digest: str = Field(min_length=1, max_length=255)
authenticated_context_digest: str = Field(min_length=1, max_length=255)
state: Literal[
"active",
"migration_pending",
"migrating",
"migrated",
"decrypted",
"exported",
"destroyed",
"unavailable",
] = "active"
created_at: datetime
wrapped_key_refs: list[str] = Field(default_factory=list, max_length=1000)
source_envelope_id: str | None = Field(default=None, max_length=255)
migration_id: str | None = Field(default=None, max_length=36)
metadata: dict[str, Any] = Field(default_factory=dict)
class EnvelopeRegistrationPayload(BaseModel):
envelope: EnvelopePayload
idempotency_key: str = Field(min_length=1, max_length=255)
policy_decision_ref: str = Field(min_length=1, max_length=1000)
class EnvelopeResponse(EnvelopePayload):
tenant_id: str
contract_version: str = "1"
class MigrationRequestPayload(BaseModel):
envelope_id: str = Field(min_length=1, max_length=255)
target_provider_id: str = Field(min_length=1, max_length=120)
target_vault_id: str = Field(min_length=1, max_length=255)
target_key_version: int = Field(ge=1)
target_algorithm_suite: str = Field(min_length=1, max_length=120)
mode: Literal["rewrap", "reencrypt", "decrypt", "export", "destroy"]
policy_decision_ref: str = Field(min_length=1, max_length=1000)
assurance_evidence_ref: str = Field(min_length=1, max_length=1000)
idempotency_key: str = Field(min_length=1, max_length=255)
class MigrationOutcomePayload(BaseModel):
state: Literal["succeeded", "rejected", "outcome_unknown"]
evidence_refs: list[str] = Field(default_factory=list, max_length=1000)
target_envelope: EnvelopePayload | None = None
error_code: str | None = Field(default=None, max_length=255)
class MigrationResponse(BaseModel):
migration_id: str
state: str
source_envelope_id: str
target_envelope: EnvelopeResponse | None = None
error: str | None = None
evidence_refs: list[str] = Field(default_factory=list)
provenance: dict[str, Any] = Field(default_factory=dict)
contract_version: str = "1"
class RecoveryRequestPayload(BaseModel):
vault_id: str = Field(min_length=1, max_length=255)
reason: str = Field(min_length=1, max_length=2000)
requested_scope: str = Field(min_length=1, max_length=255)
policy_decision_ref: str = Field(min_length=1, max_length=1000)
assurance_evidence_ref: str = Field(min_length=1, max_length=1000)
idempotency_key: str = Field(min_length=1, max_length=255)
expires_at: datetime
class RecoveryDecisionPayload(BaseModel):
decision: Literal["approve", "reject"]
reason: str = Field(min_length=1, max_length=2000)
assurance_evidence_ref: str = Field(min_length=1, max_length=1000)
expected_revision: int = Field(ge=1)
idempotency_key: str = Field(min_length=1, max_length=255)
class RecoveryResponse(BaseModel):
tenant_id: str
recovery_id: str
vault_id: str
state: str
requested_scope: str
quorum: int
approvals: int
rejections: int
revision: int
expires_at: datetime
policy_decision_ref: str
execution_ref: str | None = None
provenance: dict[str, Any] = Field(default_factory=dict)
contract_version: str = "1"
class DisablePreflightResponse(BaseModel):
allowed: bool
protected_count: int
unresolved_count: int
state_counts: dict[str, int]
blocking_envelope_refs: list[str]
required_actions: list[str]
generated_at: datetime
contract_version: str = "1"
__all__ = [
"DisablePreflightResponse",
"EnvelopePayload",
"EnvelopeRegistrationPayload",
"EnvelopeResponse",
"KeyLifecyclePayload",
"KeyRotationPayload",
"MigrationOutcomePayload",
"MigrationRequestPayload",
"MigrationResponse",
"RecoveryDecisionPayload",
"RecoveryRequestPayload",
"RecoveryResponse",
"VaultCreatePayload",
"VaultResponse",
]
File diff suppressed because it is too large Load Diff