fdai-deployment-cli 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Offline signed configuration packages using the existing configuration validators.
|
|
2
|
+
|
|
3
|
+
Callers own trust registration and atomic durable history. This library neither enrolls keys
|
|
4
|
+
nor imports packages into the Hub or changes the existing Agent artifact protocol.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from cryptography.exceptions import InvalidSignature
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
17
|
+
|
|
18
|
+
from fdai_deployment_cli.contracts import canonical_bytes, canonical_digest
|
|
19
|
+
from fdai_deployment_cli.lifecycle_configuration import (
|
|
20
|
+
LayerResolution,
|
|
21
|
+
validate_configuration_package_for_signing,
|
|
22
|
+
)
|
|
23
|
+
from fdai_deployment_cli.lifecycle_configuration_document import (
|
|
24
|
+
validate_configuration_document,
|
|
25
|
+
)
|
|
26
|
+
from fdai_deployment_cli.lifecycle_configuration_entities import (
|
|
27
|
+
INSTALLATION_CONFIGURATION_FORMAT,
|
|
28
|
+
InstallationConfigurationInput,
|
|
29
|
+
InstallationLayerResolution,
|
|
30
|
+
prepare_installation_configuration,
|
|
31
|
+
resolve_installation_configuration,
|
|
32
|
+
)
|
|
33
|
+
from fdai_deployment_cli.lifecycle_configuration_sealing import (
|
|
34
|
+
encryption_key_digest,
|
|
35
|
+
open_identifiers,
|
|
36
|
+
read_object,
|
|
37
|
+
seal_identifiers,
|
|
38
|
+
validate_sealed_identifiers,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_FORMAT = "fdai.lifecycle-configuration-package.v1"
|
|
42
|
+
_DIGEST = re.compile(r"[0-9a-f]{64}\Z", re.ASCII)
|
|
43
|
+
_REVISION = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})\Z", re.ASCII)
|
|
44
|
+
_MAX_BYTES = 1024 * 1024
|
|
45
|
+
_FIELDS = frozenset(
|
|
46
|
+
{
|
|
47
|
+
"format",
|
|
48
|
+
"revision_id",
|
|
49
|
+
"predecessor_digest",
|
|
50
|
+
"release_version",
|
|
51
|
+
"installation_key_digest",
|
|
52
|
+
"input_digest",
|
|
53
|
+
"configuration",
|
|
54
|
+
"sealed_keys",
|
|
55
|
+
"sealed",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class SignedConfigurationPackage:
|
|
62
|
+
"""Immutable detached-signature artifact; safe to persist without plaintext identifiers."""
|
|
63
|
+
|
|
64
|
+
payload: bytes
|
|
65
|
+
signature: bytes
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def digest(self) -> str:
|
|
69
|
+
return hashlib.sha256(self.payload).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
73
|
+
class ConfigurationPackageInput:
|
|
74
|
+
"""One complete customer revision for the existing single-Entity configuration contract."""
|
|
75
|
+
|
|
76
|
+
revision_id: str
|
|
77
|
+
release_version: str
|
|
78
|
+
configuration_schema: Mapping[str, object]
|
|
79
|
+
environment_config: Mapping[str, object]
|
|
80
|
+
entity_overrides: Sequence[Mapping[str, object]]
|
|
81
|
+
sealed_values: Mapping[str, object]
|
|
82
|
+
sealed_keys: frozenset[str]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class ConfigurationHistoryBinding:
|
|
87
|
+
"""Independent public trust for one ancestor, not current signing permission."""
|
|
88
|
+
|
|
89
|
+
configuration_key: Ed25519PublicKey
|
|
90
|
+
installation_key_digest: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_configuration_package(
|
|
94
|
+
source: ConfigurationPackageInput,
|
|
95
|
+
*,
|
|
96
|
+
signing_key: Ed25519PrivateKey,
|
|
97
|
+
installation_key: X25519PublicKey,
|
|
98
|
+
history: Sequence[SignedConfigurationPackage] = (),
|
|
99
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
100
|
+
) -> SignedConfigurationPackage:
|
|
101
|
+
"""Validate, seal, and sign a revision, or return its exact prior bytes on retry.
|
|
102
|
+
|
|
103
|
+
History must be the complete accepted chain in order. A caller must serialize builds
|
|
104
|
+
and persist the result atomically; passing an empty or stale history cannot prove that.
|
|
105
|
+
No key material is read from files or discovered from the environment.
|
|
106
|
+
"""
|
|
107
|
+
sealed_values = read_object(canonical_bytes(source.sealed_values))
|
|
108
|
+
if set(sealed_values) != source.sealed_keys:
|
|
109
|
+
raise ValueError("sealed identifier values do not match the trusted sealed keys")
|
|
110
|
+
configuration = read_object(
|
|
111
|
+
canonical_bytes(
|
|
112
|
+
{
|
|
113
|
+
"schema": source.configuration_schema,
|
|
114
|
+
"environment": source.environment_config,
|
|
115
|
+
"entity_overrides": source.entity_overrides,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
validate_configuration_document(
|
|
120
|
+
configuration, source.configuration_schema, source.release_version, set(source.sealed_keys)
|
|
121
|
+
)
|
|
122
|
+
return _build_signed_revision(
|
|
123
|
+
revision_id=source.revision_id,
|
|
124
|
+
release_version=source.release_version,
|
|
125
|
+
configuration=configuration,
|
|
126
|
+
sealed_values=sealed_values,
|
|
127
|
+
sealed_keys=source.sealed_keys,
|
|
128
|
+
package_format=_FORMAT,
|
|
129
|
+
release_digest=None,
|
|
130
|
+
signing_key=signing_key,
|
|
131
|
+
installation_key=installation_key,
|
|
132
|
+
history=history,
|
|
133
|
+
history_bindings=history_bindings,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _build_signed_revision(
|
|
138
|
+
*,
|
|
139
|
+
revision_id: str,
|
|
140
|
+
release_version: str,
|
|
141
|
+
configuration: dict[str, object],
|
|
142
|
+
sealed_values: dict[str, object],
|
|
143
|
+
sealed_keys: frozenset[str],
|
|
144
|
+
package_format: str,
|
|
145
|
+
release_digest: str | None,
|
|
146
|
+
signing_key: Ed25519PrivateKey,
|
|
147
|
+
installation_key: X25519PublicKey,
|
|
148
|
+
history: Sequence[SignedConfigurationPackage],
|
|
149
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
150
|
+
) -> SignedConfigurationPackage:
|
|
151
|
+
recipient_digest = encryption_key_digest(installation_key)
|
|
152
|
+
chain = _verify_history(history, signing_key.public_key(), recipient_digest, history_bindings)
|
|
153
|
+
validate_configuration_package_for_signing(sealed_values)
|
|
154
|
+
_revision(revision_id)
|
|
155
|
+
_sealed_keys(sorted(sealed_keys))
|
|
156
|
+
inputs: dict[str, object] = {
|
|
157
|
+
"configuration": configuration,
|
|
158
|
+
"release_version": release_version,
|
|
159
|
+
"sealed_values": sealed_values,
|
|
160
|
+
"installation_key_digest": recipient_digest,
|
|
161
|
+
"sealed_keys": sorted(sealed_keys),
|
|
162
|
+
}
|
|
163
|
+
if release_digest is not None:
|
|
164
|
+
if not _DIGEST.fullmatch(release_digest):
|
|
165
|
+
raise ValueError("configuration Release manifest digest is invalid")
|
|
166
|
+
inputs["release_digest"] = release_digest
|
|
167
|
+
read_object(canonical_bytes(inputs))
|
|
168
|
+
input_digest = canonical_digest(inputs)
|
|
169
|
+
for package, previous in zip(history, chain, strict=True):
|
|
170
|
+
if previous["revision_id"] == revision_id:
|
|
171
|
+
if package != history[-1] or previous["input_digest"] != input_digest:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
"configuration revision was already packaged with different inputs"
|
|
174
|
+
)
|
|
175
|
+
return package
|
|
176
|
+
document: dict[str, object] = {
|
|
177
|
+
"format": package_format,
|
|
178
|
+
"revision_id": revision_id,
|
|
179
|
+
"predecessor_digest": history[-1].digest if history else None,
|
|
180
|
+
"release_version": release_version,
|
|
181
|
+
"installation_key_digest": recipient_digest,
|
|
182
|
+
"input_digest": input_digest,
|
|
183
|
+
"configuration": configuration,
|
|
184
|
+
"sealed_keys": sorted(sealed_keys),
|
|
185
|
+
}
|
|
186
|
+
if release_digest is not None:
|
|
187
|
+
document["release_digest"] = release_digest
|
|
188
|
+
context = canonical_bytes(document)
|
|
189
|
+
document["sealed"] = seal_identifiers(sealed_values, installation_key, context=context)
|
|
190
|
+
payload = canonical_bytes(document)
|
|
191
|
+
if len(payload) > _MAX_BYTES:
|
|
192
|
+
raise ValueError("configuration package exceeds the size limit")
|
|
193
|
+
return SignedConfigurationPackage(payload, signing_key.sign(payload))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def verify_configuration_package(
|
|
197
|
+
package: SignedConfigurationPackage,
|
|
198
|
+
*,
|
|
199
|
+
configuration_key: Ed25519PublicKey,
|
|
200
|
+
installation_key_digest: str,
|
|
201
|
+
configuration_schema: Mapping[str, object],
|
|
202
|
+
sealed_keys: frozenset[str],
|
|
203
|
+
history: Sequence[SignedConfigurationPackage] = (),
|
|
204
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
205
|
+
) -> LayerResolution:
|
|
206
|
+
"""Verify an import candidate against trusted schema, recipient, and complete history.
|
|
207
|
+
|
|
208
|
+
An exact retry of the current head is a no-op. An older revision cannot become current.
|
|
209
|
+
The returned values are public configuration only, never decrypted identifiers.
|
|
210
|
+
"""
|
|
211
|
+
document = _verify_candidate(
|
|
212
|
+
package, configuration_key, installation_key_digest, sealed_keys, history, history_bindings
|
|
213
|
+
)
|
|
214
|
+
if document["format"] != _FORMAT:
|
|
215
|
+
raise ValueError("single-Entity reader cannot consume an installation revision")
|
|
216
|
+
configuration = _object(document["configuration"])
|
|
217
|
+
release_version = _text(document["release_version"])
|
|
218
|
+
return validate_configuration_document(
|
|
219
|
+
configuration, configuration_schema, release_version, _sealed_keys(document["sealed_keys"])
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def decrypt_configuration_identifiers(
|
|
224
|
+
package: SignedConfigurationPackage,
|
|
225
|
+
*,
|
|
226
|
+
configuration_key: Ed25519PublicKey,
|
|
227
|
+
installation_key: X25519PrivateKey,
|
|
228
|
+
configuration_schema: Mapping[str, object],
|
|
229
|
+
sealed_keys: frozenset[str],
|
|
230
|
+
history: Sequence[SignedConfigurationPackage] = (),
|
|
231
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
232
|
+
) -> dict[str, object]:
|
|
233
|
+
"""Authenticate the package and its chain before installation-local decryption."""
|
|
234
|
+
verify_configuration_package(
|
|
235
|
+
package,
|
|
236
|
+
configuration_key=configuration_key,
|
|
237
|
+
installation_key_digest=encryption_key_digest(installation_key.public_key()),
|
|
238
|
+
configuration_schema=configuration_schema,
|
|
239
|
+
sealed_keys=sealed_keys,
|
|
240
|
+
history=history,
|
|
241
|
+
history_bindings=history_bindings,
|
|
242
|
+
)
|
|
243
|
+
return _decrypt_values(package, installation_key)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _decrypt_values(
|
|
247
|
+
package: SignedConfigurationPackage, installation_key: X25519PrivateKey
|
|
248
|
+
) -> dict[str, object]:
|
|
249
|
+
document = read_object(package.payload)
|
|
250
|
+
envelope = _object(document.pop("sealed"))
|
|
251
|
+
values = open_identifiers(envelope, installation_key, context=canonical_bytes(document))
|
|
252
|
+
if document["format"] == _FORMAT and set(values) != _sealed_keys(document["sealed_keys"]):
|
|
253
|
+
raise ValueError("sealed identifier keys do not match the manifest")
|
|
254
|
+
inputs = {
|
|
255
|
+
"configuration": document["configuration"],
|
|
256
|
+
"release_version": document["release_version"],
|
|
257
|
+
"sealed_values": values,
|
|
258
|
+
"installation_key_digest": document["installation_key_digest"],
|
|
259
|
+
"sealed_keys": document["sealed_keys"],
|
|
260
|
+
}
|
|
261
|
+
if document["format"] == INSTALLATION_CONFIGURATION_FORMAT:
|
|
262
|
+
inputs["release_digest"] = document["release_digest"]
|
|
263
|
+
if canonical_digest(inputs) != document["input_digest"]:
|
|
264
|
+
raise ValueError("configuration input digest does not match")
|
|
265
|
+
return values
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _verify_history(
|
|
269
|
+
history: Sequence[SignedConfigurationPackage],
|
|
270
|
+
key: Ed25519PublicKey,
|
|
271
|
+
recipient_digest: str,
|
|
272
|
+
bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
273
|
+
) -> list[dict[str, object]]:
|
|
274
|
+
if len(history) > 256:
|
|
275
|
+
raise ValueError("configuration history exceeds its bound")
|
|
276
|
+
if bindings is not None and len(bindings) != len(history):
|
|
277
|
+
raise ValueError("configuration history verification bindings are incomplete")
|
|
278
|
+
chain: list[dict[str, object]] = []
|
|
279
|
+
predecessor: str | None = None
|
|
280
|
+
for index, package in enumerate(history):
|
|
281
|
+
binding = bindings[index] if bindings is not None else None
|
|
282
|
+
document = _verified_document(
|
|
283
|
+
package,
|
|
284
|
+
binding.configuration_key if binding else key,
|
|
285
|
+
binding.installation_key_digest if binding else recipient_digest,
|
|
286
|
+
)
|
|
287
|
+
_check_successor(document, predecessor, chain)
|
|
288
|
+
chain.append(document)
|
|
289
|
+
predecessor = package.digest
|
|
290
|
+
return chain
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def verify_configuration_history(
|
|
294
|
+
history: Sequence[SignedConfigurationPackage],
|
|
295
|
+
*,
|
|
296
|
+
bindings: Sequence[ConfigurationHistoryBinding],
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Authenticate a retained chain with exact per-ancestor public trust, not current authority."""
|
|
299
|
+
if len(history) != len(bindings):
|
|
300
|
+
raise ValueError("configuration history verification bindings are incomplete")
|
|
301
|
+
if history:
|
|
302
|
+
_verify_history(
|
|
303
|
+
history, bindings[0].configuration_key, bindings[0].installation_key_digest, bindings
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _check_successor(
|
|
308
|
+
document: Mapping[str, object], predecessor: str | None, chain: Sequence[Mapping[str, object]]
|
|
309
|
+
) -> None:
|
|
310
|
+
if document["predecessor_digest"] != predecessor:
|
|
311
|
+
raise ValueError("configuration predecessor gap or fork")
|
|
312
|
+
if any(item["revision_id"] == document["revision_id"] for item in chain):
|
|
313
|
+
raise ValueError("configuration revision was already imported")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _verified_document(
|
|
317
|
+
package: SignedConfigurationPackage, key: Ed25519PublicKey, recipient_digest: str
|
|
318
|
+
) -> dict[str, object]:
|
|
319
|
+
if not package.signature:
|
|
320
|
+
raise ValueError("configuration package signature is missing")
|
|
321
|
+
if len(package.payload) > _MAX_BYTES:
|
|
322
|
+
raise ValueError("configuration package exceeds the size limit")
|
|
323
|
+
try:
|
|
324
|
+
key.verify(package.signature, package.payload)
|
|
325
|
+
except InvalidSignature:
|
|
326
|
+
raise ValueError("configuration package signature is invalid") from None
|
|
327
|
+
document = read_object(package.payload)
|
|
328
|
+
package_format = document.get("format")
|
|
329
|
+
fields = _FIELDS | (
|
|
330
|
+
{"release_digest"} if package_format == INSTALLATION_CONFIGURATION_FORMAT else set()
|
|
331
|
+
)
|
|
332
|
+
if set(document) != fields or package_format not in (
|
|
333
|
+
_FORMAT,
|
|
334
|
+
INSTALLATION_CONFIGURATION_FORMAT,
|
|
335
|
+
):
|
|
336
|
+
raise ValueError("configuration package fields or format are invalid")
|
|
337
|
+
if canonical_bytes(document) != package.payload:
|
|
338
|
+
raise ValueError("configuration package must use canonical JSON")
|
|
339
|
+
_revision(_text(document["revision_id"]))
|
|
340
|
+
for name in ("installation_key_digest", "input_digest"):
|
|
341
|
+
if not _DIGEST.fullmatch(_text(document[name])):
|
|
342
|
+
raise ValueError("configuration package digest is invalid")
|
|
343
|
+
if package_format == INSTALLATION_CONFIGURATION_FORMAT and not _DIGEST.fullmatch(
|
|
344
|
+
_text(document["release_digest"])
|
|
345
|
+
):
|
|
346
|
+
raise ValueError("configuration Release manifest digest is invalid")
|
|
347
|
+
predecessor = document["predecessor_digest"]
|
|
348
|
+
if predecessor is not None and not _DIGEST.fullmatch(_text(predecessor)):
|
|
349
|
+
raise ValueError("configuration predecessor digest is invalid")
|
|
350
|
+
if document["installation_key_digest"] != recipient_digest:
|
|
351
|
+
raise ValueError("configuration package belongs to another installation key")
|
|
352
|
+
_sealed_keys(document["sealed_keys"])
|
|
353
|
+
validate_sealed_identifiers(_object(document["sealed"]), recipient_digest=recipient_digest)
|
|
354
|
+
return document
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _verify_candidate(
|
|
358
|
+
package: SignedConfigurationPackage,
|
|
359
|
+
configuration_key: Ed25519PublicKey,
|
|
360
|
+
recipient_digest: str,
|
|
361
|
+
sealed_keys: frozenset[str],
|
|
362
|
+
history: Sequence[SignedConfigurationPackage],
|
|
363
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
364
|
+
) -> dict[str, object]:
|
|
365
|
+
chain = _verify_history(history, configuration_key, recipient_digest, history_bindings)
|
|
366
|
+
document = _verified_document(package, configuration_key, recipient_digest)
|
|
367
|
+
if _sealed_keys(document["sealed_keys"]) != sealed_keys:
|
|
368
|
+
raise ValueError("sealed identifier keys do not match the trusted Release keys")
|
|
369
|
+
if not history or package != history[-1]:
|
|
370
|
+
_check_successor(document, history[-1].digest if history else None, chain)
|
|
371
|
+
return document
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def build_installation_configuration_package(
|
|
375
|
+
source: InstallationConfigurationInput,
|
|
376
|
+
*,
|
|
377
|
+
entity_ids: frozenset[str],
|
|
378
|
+
signing_key: Ed25519PrivateKey,
|
|
379
|
+
installation_key: X25519PublicKey,
|
|
380
|
+
history: Sequence[SignedConfigurationPackage] = (),
|
|
381
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
382
|
+
) -> SignedConfigurationPackage:
|
|
383
|
+
"""Produce one signed full revision, checked against the independently selected Entity set."""
|
|
384
|
+
configuration, sealed_values = prepare_installation_configuration(source, entity_ids=entity_ids)
|
|
385
|
+
return _build_signed_revision(
|
|
386
|
+
revision_id=source.revision_id,
|
|
387
|
+
release_version=source.release_version,
|
|
388
|
+
configuration=configuration,
|
|
389
|
+
sealed_values=sealed_values,
|
|
390
|
+
sealed_keys=source.sealed_keys,
|
|
391
|
+
package_format=INSTALLATION_CONFIGURATION_FORMAT,
|
|
392
|
+
release_digest=source.release_digest,
|
|
393
|
+
signing_key=signing_key,
|
|
394
|
+
installation_key=installation_key,
|
|
395
|
+
history=history,
|
|
396
|
+
history_bindings=history_bindings,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def verify_installation_configuration_package(
|
|
401
|
+
package: SignedConfigurationPackage,
|
|
402
|
+
*,
|
|
403
|
+
configuration_key: Ed25519PublicKey,
|
|
404
|
+
installation_key_digest: str,
|
|
405
|
+
configuration_schema: Mapping[str, object],
|
|
406
|
+
sealed_keys: frozenset[str],
|
|
407
|
+
entity_ids: frozenset[str],
|
|
408
|
+
release_version: str,
|
|
409
|
+
release_digest: str,
|
|
410
|
+
history: Sequence[SignedConfigurationPackage] = (),
|
|
411
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
412
|
+
) -> InstallationLayerResolution:
|
|
413
|
+
"""Authenticate the full revision against independent Release, selection, keys and history."""
|
|
414
|
+
document = _verify_candidate(
|
|
415
|
+
package, configuration_key, installation_key_digest, sealed_keys, history, history_bindings
|
|
416
|
+
)
|
|
417
|
+
if (
|
|
418
|
+
document["format"] != INSTALLATION_CONFIGURATION_FORMAT
|
|
419
|
+
or document["release_version"] != release_version
|
|
420
|
+
or document["release_digest"] != release_digest
|
|
421
|
+
):
|
|
422
|
+
raise ValueError("installation configuration format or Release does not match")
|
|
423
|
+
return resolve_installation_configuration(
|
|
424
|
+
_object(document["configuration"]),
|
|
425
|
+
configuration_schema=configuration_schema,
|
|
426
|
+
release_version=release_version,
|
|
427
|
+
sealed_keys=sealed_keys,
|
|
428
|
+
entity_ids=entity_ids,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def decrypt_installation_configuration_identifiers(
|
|
433
|
+
package: SignedConfigurationPackage,
|
|
434
|
+
*,
|
|
435
|
+
entity_id: str,
|
|
436
|
+
configuration_key: Ed25519PublicKey,
|
|
437
|
+
installation_key: X25519PrivateKey,
|
|
438
|
+
configuration_schema: Mapping[str, object],
|
|
439
|
+
sealed_keys: frozenset[str],
|
|
440
|
+
entity_ids: frozenset[str],
|
|
441
|
+
release_version: str,
|
|
442
|
+
release_digest: str,
|
|
443
|
+
history: Sequence[SignedConfigurationPackage] = (),
|
|
444
|
+
history_bindings: Sequence[ConfigurationHistoryBinding] | None = None,
|
|
445
|
+
) -> dict[str, object]:
|
|
446
|
+
"""Verify the whole revision before returning identifiers for exactly one selected Entity."""
|
|
447
|
+
verify_installation_configuration_package(
|
|
448
|
+
package,
|
|
449
|
+
configuration_key=configuration_key,
|
|
450
|
+
installation_key_digest=encryption_key_digest(installation_key.public_key()),
|
|
451
|
+
configuration_schema=configuration_schema,
|
|
452
|
+
sealed_keys=sealed_keys,
|
|
453
|
+
entity_ids=entity_ids,
|
|
454
|
+
release_version=release_version,
|
|
455
|
+
release_digest=release_digest,
|
|
456
|
+
history=history,
|
|
457
|
+
history_bindings=history_bindings,
|
|
458
|
+
)
|
|
459
|
+
if entity_id not in entity_ids:
|
|
460
|
+
raise ValueError("requested Entity is not selected")
|
|
461
|
+
values = _decrypt_values(package, installation_key)
|
|
462
|
+
if set(values) != entity_ids or any(
|
|
463
|
+
set(_object(item)) != sealed_keys for item in values.values()
|
|
464
|
+
):
|
|
465
|
+
raise ValueError("sealed Entity identifier coverage does not match")
|
|
466
|
+
return _object(values[entity_id])
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _sealed_keys(value: object) -> set[str]:
|
|
470
|
+
if not isinstance(value, list) or any(not isinstance(item, str) or not item for item in value):
|
|
471
|
+
raise ValueError("sealed identifier keys are invalid")
|
|
472
|
+
keys = {_text(item) for item in value}
|
|
473
|
+
if len(keys) != len(value):
|
|
474
|
+
raise ValueError("sealed identifier keys are duplicated")
|
|
475
|
+
return keys
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _object(value: object) -> dict[str, object]:
|
|
479
|
+
if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
|
|
480
|
+
raise ValueError("configuration field must be an object")
|
|
481
|
+
return {str(key): item for key, item in value.items()}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _text(value: object) -> str:
|
|
485
|
+
if not isinstance(value, str):
|
|
486
|
+
raise ValueError("configuration field must be text")
|
|
487
|
+
return value
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _revision(value: str) -> None:
|
|
491
|
+
if not _REVISION.fullmatch(value):
|
|
492
|
+
raise ValueError("configuration revision must be a full Git commit identifier")
|