1use super::*;
2use serde::Serialize;
3use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Serialize)]
6struct LifecycleAuthorityReportDigestInput<'a> {
7 report_id: &'a str,
8 check_id: &'a str,
9 plan_id: &'a str,
10 inventory_id: &'a str,
11 authorities: &'a [LifecycleAuthorityV1],
12 external_action_required_count: usize,
13 blocked_count: usize,
14}
15
16#[derive(Serialize)]
17struct ExternalLifecyclePlanDigestInput<'a> {
18 lifecycle_authority_report_id: &'a str,
19 deployment_plan_id: &'a str,
20 deployment_plan_digest: &'a str,
21 inventory_id: &'a str,
22 lifecycle_authority_rows: &'a [LifecycleAuthorityV1],
23 directly_executable_role_upgrades: &'a [ExternalLifecycleRoleUpgradeV1],
24 proposed_external_role_upgrades: &'a [ExternalLifecycleRoleUpgradeV1],
25 blocked_role_upgrades: &'a [ExternalLifecycleRoleUpgradeV1],
26 dependency_blockers: &'a [String],
27 protected_call_implications: &'a [String],
28 residual_exposure: &'a [String],
29 status: ExternalLifecyclePlanStatusV1,
30}
31
32#[derive(Serialize)]
33struct ExternalUpgradeProposalReportDigestInput<'a> {
34 report_id: &'a str,
35 lifecycle_plan_id: &'a str,
36 lifecycle_plan_digest: &'a str,
37 deployment_plan_id: &'a str,
38 deployment_plan_digest: &'a str,
39 inventory_id: &'a str,
40 proposals: &'a [ExternalUpgradeProposalV1],
41 blocked_subjects: &'a [String],
42}
43
44#[derive(Serialize)]
45struct ExternalLifecyclePendingReportDigestInput<'a> {
46 report_id: &'a str,
47 lifecycle_plan_id: &'a str,
48 lifecycle_plan_digest: &'a str,
49 proposal_report_id: &'a str,
50 proposal_report_digest: &'a str,
51 deployment_plan_id: &'a str,
52 deployment_plan_digest: &'a str,
53 inventory_id: &'a str,
54 direct_upgrade_count: usize,
55 pending_external_count: usize,
56 blocked_count: usize,
57 pending_external_actions: &'a [ExternalLifecyclePendingActionV1],
58 blocked_subjects: &'a [String],
59 residual_exposure: &'a [String],
60 status: ExternalLifecyclePlanStatusV1,
61}
62
63#[derive(Serialize)]
64struct ExternalLifecycleCheckDigestInput<'a> {
65 check_id: &'a str,
66 lifecycle_plan_id: &'a str,
67 lifecycle_plan_digest: &'a str,
68 proposal_report_id: &'a str,
69 proposal_report_digest: &'a str,
70 pending_report_id: &'a str,
71 pending_report_digest: &'a str,
72 deployment_plan_id: &'a str,
73 deployment_plan_digest: &'a str,
74 inventory_id: &'a str,
75 status: ExternalLifecyclePlanStatusV1,
76 direct_upgrade_count: usize,
77 pending_external_count: usize,
78 blocked_count: usize,
79 residual_exposure_count: usize,
80 summary: &'a str,
81 next_actions: &'a [String],
82}
83
84#[derive(Serialize)]
85struct ExternalLifecycleHandoffDigestInput<'a> {
86 handoff_id: &'a str,
87 lifecycle_check_id: &'a str,
88 lifecycle_check_digest: &'a str,
89 pending_report_id: &'a str,
90 pending_report_digest: &'a str,
91 proposal_report_id: &'a str,
92 proposal_report_digest: &'a str,
93 deployment_plan_id: &'a str,
94 deployment_plan_digest: &'a str,
95 inventory_id: &'a str,
96 status: ExternalLifecyclePlanStatusV1,
97 handoff_actions: &'a [ExternalLifecycleHandoffActionV1],
98 blocked_subjects: &'a [String],
99 residual_exposure: &'a [String],
100 operator_summary: &'a str,
101}
102
103#[derive(Serialize)]
104struct CriticalExternalFixReportDigestInput<'a> {
105 report_id: &'a str,
106 fix_id: &'a str,
107 severity: &'a str,
108 lifecycle_plan_id: &'a str,
109 lifecycle_plan_digest: &'a str,
110 pending_report_id: &'a str,
111 pending_report_digest: &'a str,
112 deployment_plan_id: &'a str,
113 deployment_plan_digest: &'a str,
114 inventory_id: &'a str,
115 affected_roles: &'a [String],
116 affected_canisters: &'a [String],
117 directly_patchable_roles: &'a [String],
118 externally_blocked_roles: &'a [String],
119 dependency_blocked_roles: &'a [String],
120 required_external_actions: &'a [String],
121 protected_call_implications: &'a [String],
122 residual_exposure: &'a [String],
123 operator_next_steps: &'a [String],
124}
125
126#[derive(Serialize)]
127struct ExternalUpgradeProposalDigestInput<'a> {
128 deployment_plan_id: &'a str,
129 deployment_plan_digest: &'a str,
130 lifecycle_plan_id: &'a str,
131 lifecycle_plan_digest: &'a str,
132 promotion_plan_id: &'a Option<String>,
133 promotion_plan_digest: &'a Option<String>,
134 promotion_provenance_id: &'a Option<String>,
135 promotion_provenance_digest: &'a Option<String>,
136 subject: &'a str,
137 canister_id: &'a Option<String>,
138 role: &'a Option<String>,
139 control_class: CanisterControlClassV1,
140 lifecycle_mode: LifecycleModeV1,
141 observed_before_digest: &'a str,
142 current_module_hash: &'a Option<String>,
143 current_canonical_embedded_config_sha256: &'a Option<String>,
144 target_wasm_sha256: &'a Option<String>,
145 target_wasm_gz_sha256: &'a Option<String>,
146 target_installed_module_hash: &'a Option<String>,
147 target_role_artifact_identity: &'a Option<String>,
148 target_canonical_embedded_config_sha256: &'a Option<String>,
149 root_trust_anchor: &'a Option<String>,
150 authority_profile_hash: &'a Option<String>,
151 required_external_action: &'a str,
152 consent_requirements: &'a [ConsentRequirementV1],
153 allowed_authorization_modes: &'a [ExternalUpgradeAuthorizationModeV1],
154 verification_requirements: &'a [LifecycleVerificationRequirementV1],
155 expires_at: &'a Option<String>,
156 supersedes_proposal_id: &'a Option<String>,
157}
158
159#[derive(Serialize)]
160struct ExternalUpgradeReceiptDigestInput<'a> {
161 proposal_id: &'a str,
162 proposal_digest: &'a str,
163 subject: &'a str,
164 canister_id: &'a Option<String>,
165 role: &'a Option<String>,
166 consent_state: ExternalUpgradeConsentStateV1,
167 reported_by: &'a Option<String>,
168 observed_before_module_hash: &'a Option<String>,
169 observed_after_module_hash: &'a Option<String>,
170 observed_after_canonical_embedded_config_sha256: &'a Option<String>,
171 verification_result: ExternalUpgradeVerificationResultV1,
172 verification_notes: &'a [String],
173}
174
175#[derive(Serialize)]
176struct ExternalUpgradeConsentEvidenceDigestInput<'a> {
177 evidence_id: &'a str,
178 proposal_id: &'a str,
179 proposal_digest: &'a str,
180 receipt_id: &'a str,
181 receipt_digest: &'a str,
182 subject: &'a str,
183 canister_id: &'a Option<String>,
184 role: &'a Option<String>,
185 consent_state: ExternalUpgradeConsentStateV1,
186 reported_by: &'a Option<String>,
187 consent_requirements: &'a [ConsentRequirementV1],
188 allowed_authorization_modes: &'a [ExternalUpgradeAuthorizationModeV1],
189 status_summary: &'a str,
190}
191
192#[derive(Serialize)]
193struct ExternalUpgradeVerificationReportDigestInput<'a> {
194 report_id: &'a str,
195 proposal_id: &'a str,
196 proposal_digest: &'a str,
197 receipt_id: &'a str,
198 receipt_digest: &'a str,
199 subject: &'a str,
200 canister_id: &'a Option<String>,
201 role: &'a Option<String>,
202 verification_result: ExternalUpgradeVerificationResultV1,
203 verification_notes: &'a [String],
204 live_inventory_required: bool,
205 status_summary: &'a str,
206}
207
208#[derive(Serialize)]
209struct ExternalUpgradeVerificationPolicyDigestInput<'a> {
210 policy_id: &'a str,
211 proposal_id: &'a str,
212 proposal_digest: &'a str,
213 subject: &'a str,
214 canister_id: &'a Option<String>,
215 role: &'a Option<String>,
216 required_verification: &'a [LifecycleVerificationRequirementV1],
217 verification_requirements: &'a [ExternalUpgradeVerificationPolicyRequirementV1],
218 max_observation_age_seconds: Option<u64>,
219 status_summary: &'a str,
220}
221
222#[derive(Serialize)]
223struct ExternalUpgradeVerificationCheckDigestInput<'a> {
224 check_id: &'a str,
225 policy_id: &'a str,
226 policy_digest: &'a str,
227 proposal_id: &'a str,
228 proposal_digest: &'a str,
229 subject: &'a str,
230 canister_id: &'a Option<String>,
231 role: &'a Option<String>,
232 observation: &'a ExternalUpgradeVerificationObservationV1,
233 requirement_results: &'a [ExternalUpgradeVerificationCheckRequirementV1],
234 verification_result: ExternalUpgradeVerificationResultV1,
235 status_summary: &'a str,
236}
237
238#[derive(Serialize)]
239struct ExternalUpgradeCompletionReportDigestInput<'a> {
240 report_id: &'a str,
241 proposal_id: &'a str,
242 proposal_digest: &'a str,
243 consent_evidence_id: &'a str,
244 consent_evidence_digest: &'a str,
245 verification_check_id: &'a str,
246 verification_check_digest: &'a str,
247 subject: &'a str,
248 canister_id: &'a Option<String>,
249 role: &'a Option<String>,
250 consent_state: ExternalUpgradeConsentStateV1,
251 verification_result: ExternalUpgradeVerificationResultV1,
252 completion_status: ExternalUpgradeCompletionStatusV1,
253 blockers: &'a [String],
254 next_actions: &'a [String],
255 status_summary: &'a str,
256}
257
258#[derive(Serialize)]
259struct ObservedBeforeDigestInput<'a> {
260 subject: &'a str,
261 canister_id: &'a Option<String>,
262 role: &'a Option<String>,
263 observed_controllers: &'a [String],
264 current_module_hash: Option<&'a String>,
265 current_canonical_embedded_config_sha256: Option<&'a String>,
266}
267
268#[derive(Debug, Eq, thiserror::Error, PartialEq)]
272pub enum ExternalUpgradeReceiptError {
273 #[error("external upgrade receipt schema version {actual} does not match expected {expected}")]
274 SchemaVersionMismatch { expected: u32, actual: u32 },
275 #[error("external upgrade receipt field `{field}` is required")]
276 MissingRequiredField { field: &'static str },
277 #[error("external upgrade receipt field `{field}` digest is stale")]
278 DigestMismatch { field: &'static str },
279 #[error("external upgrade receipt field `{field}` does not match proposal source")]
280 SourceMismatch { field: &'static str },
281 #[error("external upgrade receipt verification result does not match observations")]
282 VerificationMismatch,
283 #[error("external upgrade receipt refused consent cannot be verified")]
284 RefusedConsentVerified,
285}
286
287#[derive(Debug, Eq, thiserror::Error, PartialEq)]
291pub enum ExternalUpgradeConsentEvidenceError {
292 #[error(
293 "external upgrade consent evidence schema version {actual} does not match expected {expected}"
294 )]
295 SchemaVersionMismatch { expected: u32, actual: u32 },
296 #[error("external upgrade consent evidence field `{field}` is required")]
297 MissingRequiredField { field: &'static str },
298 #[error("external upgrade consent evidence field `{field}` digest is stale")]
299 DigestMismatch { field: &'static str },
300 #[error("external upgrade consent evidence field `{field}` no longer matches source receipt")]
301 SourceMismatch { field: &'static str },
302 #[error(transparent)]
303 Receipt(#[from] ExternalUpgradeReceiptError),
304}
305
306#[derive(Debug, Eq, thiserror::Error, PartialEq)]
310pub enum ExternalUpgradeVerificationReportError {
311 #[error(
312 "external upgrade verification report schema version {actual} does not match expected {expected}"
313 )]
314 SchemaVersionMismatch { expected: u32, actual: u32 },
315 #[error("external upgrade verification report field `{field}` is required")]
316 MissingRequiredField { field: &'static str },
317 #[error("external upgrade verification report field `{field}` digest is stale")]
318 DigestMismatch { field: &'static str },
319 #[error("external upgrade verification report field `{field}` does not match source evidence")]
320 SourceMismatch { field: &'static str },
321 #[error(transparent)]
322 Receipt(#[from] ExternalUpgradeReceiptError),
323}
324
325#[derive(Debug, Eq, thiserror::Error, PartialEq)]
329pub enum ExternalUpgradeVerificationPolicyError {
330 #[error(
331 "external upgrade verification policy schema version {actual} does not match expected {expected}"
332 )]
333 SchemaVersionMismatch { expected: u32, actual: u32 },
334 #[error("external upgrade verification policy field `{field}` is required")]
335 MissingRequiredField { field: &'static str },
336 #[error("external upgrade verification policy field `{field}` digest is stale")]
337 DigestMismatch { field: &'static str },
338 #[error("external upgrade verification policy field `{field}` does not match proposal source")]
339 SourceMismatch { field: &'static str },
340}
341
342#[derive(Debug, Eq, thiserror::Error, PartialEq)]
346pub enum ExternalUpgradeVerificationCheckError {
347 #[error(
348 "external upgrade verification check schema version {actual} does not match expected {expected}"
349 )]
350 SchemaVersionMismatch { expected: u32, actual: u32 },
351 #[error("external upgrade verification check field `{field}` is required")]
352 MissingRequiredField { field: &'static str },
353 #[error("external upgrade verification check field `{field}` digest is stale")]
354 DigestMismatch { field: &'static str },
355 #[error("external upgrade verification check field `{field}` does not match policy source")]
356 SourceMismatch { field: &'static str },
357 #[error("external upgrade verification check contains duplicate requirement `{requirement:?}`")]
358 DuplicateRequirement {
359 requirement: LifecycleVerificationRequirementV1,
360 },
361 #[error(
362 "external upgrade verification check requirement `{requirement:?}` has invalid satisfaction state"
363 )]
364 RequirementStatusMismatch {
365 requirement: LifecycleVerificationRequirementV1,
366 },
367}
368
369#[derive(Debug, Eq, thiserror::Error, PartialEq)]
373pub enum ExternalUpgradeCompletionReportError {
374 #[error(
375 "external upgrade completion report schema version {actual} does not match expected {expected}"
376 )]
377 SchemaVersionMismatch { expected: u32, actual: u32 },
378 #[error("external upgrade completion report field `{field}` is required")]
379 MissingRequiredField { field: &'static str },
380 #[error("external upgrade completion report field `{field}` digest is stale")]
381 DigestMismatch { field: &'static str },
382 #[error("external upgrade completion report field `{field}` does not match source evidence")]
383 SourceMismatch { field: &'static str },
384 #[error(transparent)]
385 Proposal(#[from] ExternalUpgradeProposalReportError),
386 #[error(transparent)]
387 ConsentEvidence(#[from] ExternalUpgradeConsentEvidenceError),
388 #[error(transparent)]
389 VerificationCheck(#[from] ExternalUpgradeVerificationCheckError),
390}
391
392#[derive(Debug, Eq, thiserror::Error, PartialEq)]
396pub enum LifecycleAuthorityReportError {
397 #[error(
398 "lifecycle authority report schema version {actual} does not match expected {expected}"
399 )]
400 SchemaVersionMismatch { expected: u32, actual: u32 },
401 #[error("lifecycle authority report field `{field}` is required")]
402 MissingRequiredField { field: &'static str },
403 #[error("lifecycle authority report field `{field}` digest is stale")]
404 DigestMismatch { field: &'static str },
405 #[error("lifecycle authority report contains duplicate subject `{subject}`")]
406 DuplicateSubject { subject: String },
407 #[error("lifecycle authority report counters do not match authority rows")]
408 CountMismatch,
409}
410
411#[derive(Debug, Eq, thiserror::Error, PartialEq)]
415pub enum ExternalLifecyclePlanError {
416 #[error("external lifecycle plan schema version {actual} does not match expected {expected}")]
417 SchemaVersionMismatch { expected: u32, actual: u32 },
418 #[error("external lifecycle plan field `{field}` is required")]
419 MissingRequiredField { field: &'static str },
420 #[error("external lifecycle plan field `{field}` digest is stale")]
421 DigestMismatch { field: &'static str },
422 #[error("external lifecycle plan field `{field}` does not match deployment truth source")]
423 SourceMismatch { field: &'static str },
424 #[error("external lifecycle plan status does not match role partitioning")]
425 StatusMismatch,
426 #[error("external lifecycle plan contains duplicate subject `{subject}`")]
427 DuplicateSubject { subject: String },
428}
429
430#[derive(Debug, Eq, thiserror::Error, PartialEq)]
434pub enum ExternalUpgradeProposalReportError {
435 #[error(
436 "external upgrade proposal report schema version {actual} does not match expected {expected}"
437 )]
438 SchemaVersionMismatch { expected: u32, actual: u32 },
439 #[error("external upgrade proposal report field `{field}` is required")]
440 MissingRequiredField { field: &'static str },
441 #[error("external upgrade proposal report field `{field}` digest is stale")]
442 DigestMismatch { field: &'static str },
443 #[error("external upgrade proposal report field `{field}` does not match lifecycle source")]
444 SourceMismatch { field: &'static str },
445 #[error(
446 "external upgrade proposal report contains proposal for directly controlled row `{subject}`"
447 )]
448 DirectLifecycleProposal { subject: String },
449 #[error("external upgrade proposal report contains duplicate subject `{subject}`")]
450 DuplicateSubject { subject: String },
451}
452
453#[derive(Debug, Eq, thiserror::Error, PartialEq)]
457pub enum ExternalLifecyclePendingReportError {
458 #[error(
459 "external lifecycle pending report schema version {actual} does not match expected {expected}"
460 )]
461 SchemaVersionMismatch { expected: u32, actual: u32 },
462 #[error("external lifecycle pending report field `{field}` is required")]
463 MissingRequiredField { field: &'static str },
464 #[error("external lifecycle pending report field `{field}` digest is stale")]
465 DigestMismatch { field: &'static str },
466 #[error("external lifecycle pending report field `{field}` does not match lifecycle source")]
467 SourceMismatch { field: &'static str },
468 #[error("external lifecycle pending report counters do not match action rows")]
469 CountMismatch,
470 #[error("external lifecycle pending report contains duplicate subject `{subject}`")]
471 DuplicateSubject { subject: String },
472}
473
474#[derive(Debug, Eq, thiserror::Error, PartialEq)]
478pub enum ExternalLifecycleCheckError {
479 #[error("external lifecycle check schema version {actual} does not match expected {expected}")]
480 SchemaVersionMismatch { expected: u32, actual: u32 },
481 #[error("external lifecycle check field `{field}` is required")]
482 MissingRequiredField { field: &'static str },
483 #[error("external lifecycle check field `{field}` digest is stale")]
484 DigestMismatch { field: &'static str },
485 #[error("external lifecycle check field `{field}` does not match lifecycle source")]
486 SourceMismatch { field: &'static str },
487 #[error("external lifecycle check counters do not match source reports")]
488 CountMismatch,
489}
490
491#[derive(Debug, Eq, thiserror::Error, PartialEq)]
495pub enum ExternalLifecycleHandoffError {
496 #[error(
497 "external lifecycle handoff schema version {actual} does not match expected {expected}"
498 )]
499 SchemaVersionMismatch { expected: u32, actual: u32 },
500 #[error("external lifecycle handoff field `{field}` is required")]
501 MissingRequiredField { field: &'static str },
502 #[error("external lifecycle handoff field `{field}` digest is stale")]
503 DigestMismatch { field: &'static str },
504 #[error("external lifecycle handoff field `{field}` does not match lifecycle source")]
505 SourceMismatch { field: &'static str },
506 #[error("external lifecycle handoff contains duplicate subject `{subject}`")]
507 DuplicateSubject { subject: String },
508}
509
510#[derive(Debug, Eq, thiserror::Error, PartialEq)]
514pub enum CriticalExternalFixReportError {
515 #[error(
516 "critical external fix report schema version {actual} does not match expected {expected}"
517 )]
518 SchemaVersionMismatch { expected: u32, actual: u32 },
519 #[error("critical external fix report field `{field}` is required")]
520 MissingRequiredField { field: &'static str },
521 #[error("critical external fix report field `{field}` digest is stale")]
522 DigestMismatch { field: &'static str },
523 #[error("critical external fix report field `{field}` does not match lifecycle source")]
524 SourceMismatch { field: &'static str },
525}
526
527#[must_use]
531pub fn lifecycle_authority_report_from_check(
532 report_id: impl Into<String>,
533 check: &DeploymentCheckV1,
534) -> LifecycleAuthorityReportV1 {
535 let mut authorities = Vec::new();
536 let mut seen_subjects = BTreeSet::new();
537
538 for expected in &check.plan.expected_canisters {
539 let observed = observed_canister_for_expected(&check.inventory, expected);
540 let authority = lifecycle_authority_for_expected_canister(&check.plan, expected, observed);
541 seen_subjects.insert(authority.subject.clone());
542 authorities.push(authority);
543 }
544
545 for expected in &check.plan.expected_pool {
546 let observed = observed_pool_for_expected(&check.inventory, expected);
547 let authority = lifecycle_authority_for_expected_pool(expected, observed);
548 seen_subjects.insert(authority.subject.clone());
549 authorities.push(authority);
550 }
551
552 for observed in &check.inventory.observed_canisters {
553 let subject = lifecycle_subject(observed.canister_id.as_str(), observed.role.as_deref());
554 if seen_subjects.contains(&subject) {
555 continue;
556 }
557 authorities.push(lifecycle_authority_for_unplanned_canister(observed));
558 }
559
560 for observed in &check.inventory.observed_pool {
561 let subject = lifecycle_subject(observed.canister_id.as_str(), observed.role.as_deref());
562 if seen_subjects.contains(&subject) {
563 continue;
564 }
565 authorities.push(lifecycle_authority_for_unplanned_pool(observed));
566 }
567
568 authorities.sort_by(|left, right| left.subject.cmp(&right.subject));
569 let external_action_required_count = authorities
570 .iter()
571 .filter(|authority| authority.external_action_required)
572 .count();
573 let blocked_count = authorities
574 .iter()
575 .filter(|authority| authority.blocked)
576 .count();
577
578 let mut report = LifecycleAuthorityReportV1 {
579 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
580 report_id: report_id.into(),
581 report_digest: String::new(),
582 check_id: check.check_id.clone(),
583 plan_id: check.plan.plan_id.clone(),
584 inventory_id: check.inventory.inventory_id.clone(),
585 authorities,
586 external_action_required_count,
587 blocked_count,
588 };
589 report.report_digest = lifecycle_authority_report_digest(&report);
590 report
591}
592
593pub fn validate_lifecycle_authority_report(
595 report: &LifecycleAuthorityReportV1,
596) -> Result<(), LifecycleAuthorityReportError> {
597 if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
598 return Err(LifecycleAuthorityReportError::SchemaVersionMismatch {
599 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
600 actual: report.schema_version,
601 });
602 }
603 ensure_lifecycle_authority_report_field("report_id", report.report_id.as_str())?;
604 ensure_lifecycle_authority_report_field("report_digest", report.report_digest.as_str())?;
605 ensure_lifecycle_authority_report_field("check_id", report.check_id.as_str())?;
606 ensure_lifecycle_authority_report_field("plan_id", report.plan_id.as_str())?;
607 ensure_lifecycle_authority_report_field("inventory_id", report.inventory_id.as_str())?;
608 ensure_unique_authority_subjects(&report.authorities)?;
609 if report.external_action_required_count
610 != report
611 .authorities
612 .iter()
613 .filter(|authority| authority.external_action_required)
614 .count()
615 || report.blocked_count
616 != report
617 .authorities
618 .iter()
619 .filter(|authority| authority.blocked)
620 .count()
621 {
622 return Err(LifecycleAuthorityReportError::CountMismatch);
623 }
624 if report.report_digest != lifecycle_authority_report_digest(report) {
625 return Err(LifecycleAuthorityReportError::DigestMismatch {
626 field: "report_digest",
627 });
628 }
629 Ok(())
630}
631
632#[must_use]
638pub fn external_lifecycle_plan_from_check(
639 lifecycle_plan_id: impl Into<String>,
640 lifecycle_authority_report_id: impl Into<String>,
641 check: &DeploymentCheckV1,
642) -> ExternalLifecyclePlanV1 {
643 let lifecycle_authority_report =
644 lifecycle_authority_report_from_check(lifecycle_authority_report_id, check);
645 let lifecycle_authority_rows = lifecycle_authority_report.authorities;
646 let directly_executable_role_upgrades = lifecycle_authority_rows
647 .iter()
648 .filter(|authority| {
649 authority.lifecycle_mode == LifecycleModeV1::DirectDeploymentAuthority
650 && !authority.blocked
651 })
652 .map(external_lifecycle_role_upgrade)
653 .collect::<Vec<_>>();
654 let proposed_external_role_upgrades = lifecycle_authority_rows
655 .iter()
656 .filter(|authority| authority.external_action_required && !authority.blocked)
657 .map(external_lifecycle_role_upgrade)
658 .collect::<Vec<_>>();
659 let blocked_role_upgrades = lifecycle_authority_rows
660 .iter()
661 .filter(|authority| authority.blocked)
662 .map(external_lifecycle_role_upgrade)
663 .collect::<Vec<_>>();
664 let residual_exposure = proposed_external_role_upgrades
665 .iter()
666 .map(|upgrade| {
667 format!(
668 "{} remains pending external lifecycle action",
669 upgrade.subject
670 )
671 })
672 .collect::<Vec<_>>();
673 let status = if !blocked_role_upgrades.is_empty() {
674 ExternalLifecyclePlanStatusV1::Blocked
675 } else if !proposed_external_role_upgrades.is_empty() {
676 ExternalLifecyclePlanStatusV1::PendingExternalAction
677 } else {
678 ExternalLifecyclePlanStatusV1::Ready
679 };
680 let deployment_plan_digest = stable_json_sha256_hex(&check.plan);
681 let mut plan = ExternalLifecyclePlanV1 {
682 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
683 lifecycle_plan_id: lifecycle_plan_id.into(),
684 lifecycle_plan_digest: String::new(),
685 lifecycle_authority_report_id: lifecycle_authority_report.report_id,
686 deployment_plan_id: check.plan.plan_id.clone(),
687 deployment_plan_digest,
688 inventory_id: check.inventory.inventory_id.clone(),
689 lifecycle_authority_rows,
690 directly_executable_role_upgrades,
691 proposed_external_role_upgrades,
692 blocked_role_upgrades,
693 dependency_blockers: Vec::new(),
694 protected_call_implications: protected_call_implications_for_check(check),
695 residual_exposure,
696 status,
697 };
698 plan.lifecycle_plan_digest = external_lifecycle_plan_digest(&plan);
699 plan
700}
701
702pub fn validate_external_lifecycle_plan(
704 plan: &ExternalLifecyclePlanV1,
705) -> Result<(), ExternalLifecyclePlanError> {
706 if plan.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
707 return Err(ExternalLifecyclePlanError::SchemaVersionMismatch {
708 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
709 actual: plan.schema_version,
710 });
711 }
712 ensure_external_lifecycle_plan_field("lifecycle_plan_id", plan.lifecycle_plan_id.as_str())?;
713 ensure_external_lifecycle_plan_field(
714 "lifecycle_authority_report_id",
715 plan.lifecycle_authority_report_id.as_str(),
716 )?;
717 ensure_external_lifecycle_plan_field("deployment_plan_id", plan.deployment_plan_id.as_str())?;
718 ensure_external_lifecycle_plan_field("inventory_id", plan.inventory_id.as_str())?;
719 if plan.lifecycle_plan_digest != external_lifecycle_plan_digest(plan) {
720 return Err(ExternalLifecyclePlanError::DigestMismatch {
721 field: "lifecycle_plan_digest",
722 });
723 }
724 if plan.status != expected_lifecycle_plan_status(plan) {
725 return Err(ExternalLifecyclePlanError::StatusMismatch);
726 }
727 ensure_unique_lifecycle_subjects(&plan.lifecycle_authority_rows)?;
728 ensure_unique_role_upgrade_subjects(&plan.directly_executable_role_upgrades)?;
729 ensure_unique_role_upgrade_subjects(&plan.proposed_external_role_upgrades)?;
730 ensure_unique_role_upgrade_subjects(&plan.blocked_role_upgrades)?;
731 Ok(())
732}
733
734pub fn validate_external_lifecycle_plan_for_check(
737 plan: &ExternalLifecyclePlanV1,
738 check: &DeploymentCheckV1,
739) -> Result<(), ExternalLifecyclePlanError> {
740 validate_external_lifecycle_plan(plan)?;
741 let expected = external_lifecycle_plan_from_check(
742 plan.lifecycle_plan_id.clone(),
743 plan.lifecycle_authority_report_id.clone(),
744 check,
745 );
746 if plan != &expected {
747 return Err(ExternalLifecyclePlanError::SourceMismatch {
748 field: "deployment_check",
749 });
750 }
751 Ok(())
752}
753
754#[must_use]
759pub fn external_upgrade_receipt_from_observation(
760 receipt_id: impl Into<String>,
761 proposal: &ExternalUpgradeProposalV1,
762 consent_state: ExternalUpgradeConsentStateV1,
763 reported_by: Option<String>,
764 observed_after: Option<&ObservedCanisterV1>,
765) -> ExternalUpgradeReceiptV1 {
766 let observed_after_module_hash =
767 observed_after.and_then(|observed| observed.module_hash.clone());
768 let observed_after_canonical_embedded_config_sha256 =
769 observed_after.and_then(|observed| observed.canonical_embedded_config_digest.clone());
770 let verification_result = external_upgrade_verification_result(
771 consent_state,
772 proposal,
773 observed_after_module_hash.as_deref(),
774 observed_after_canonical_embedded_config_sha256.as_deref(),
775 );
776 let verification_notes = external_upgrade_verification_notes(
777 verification_result,
778 proposal,
779 observed_after_module_hash.as_deref(),
780 observed_after_canonical_embedded_config_sha256.as_deref(),
781 );
782
783 let mut receipt = ExternalUpgradeReceiptV1 {
784 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
785 receipt_id: receipt_id.into(),
786 proposal_id: proposal.proposal_id.clone(),
787 proposal_digest: proposal.proposal_digest.clone(),
788 subject: proposal.subject.clone(),
789 canister_id: proposal.canister_id.clone(),
790 role: proposal.role.clone(),
791 consent_state,
792 reported_by,
793 observed_before_module_hash: proposal.current_module_hash.clone(),
794 observed_after_module_hash,
795 observed_after_canonical_embedded_config_sha256,
796 verification_result,
797 verification_notes,
798 receipt_digest: String::new(),
799 };
800 receipt.receipt_digest = external_upgrade_receipt_digest(&receipt);
801 receipt
802}
803
804pub fn validate_external_upgrade_receipt(
809 receipt: &ExternalUpgradeReceiptV1,
810) -> Result<(), ExternalUpgradeReceiptError> {
811 if receipt.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
812 return Err(ExternalUpgradeReceiptError::SchemaVersionMismatch {
813 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
814 actual: receipt.schema_version,
815 });
816 }
817 ensure_external_receipt_field("receipt_id", receipt.receipt_id.as_str())?;
818 ensure_external_receipt_field("proposal_id", receipt.proposal_id.as_str())?;
819 ensure_external_receipt_field("proposal_digest", receipt.proposal_digest.as_str())?;
820 ensure_external_receipt_field("subject", receipt.subject.as_str())?;
821 ensure_external_receipt_field("receipt_digest", receipt.receipt_digest.as_str())?;
822
823 if receipt.consent_state == ExternalUpgradeConsentStateV1::Refused
824 && receipt.verification_result == ExternalUpgradeVerificationResultV1::Verified
825 {
826 return Err(ExternalUpgradeReceiptError::RefusedConsentVerified);
827 }
828 let has_observation = receipt.observed_after_module_hash.is_some()
829 || receipt
830 .observed_after_canonical_embedded_config_sha256
831 .is_some();
832 if matches!(
833 receipt.verification_result,
834 ExternalUpgradeVerificationResultV1::Verified
835 | ExternalUpgradeVerificationResultV1::Mismatch
836 ) && !has_observation
837 {
838 return Err(ExternalUpgradeReceiptError::VerificationMismatch);
839 }
840 if receipt.receipt_digest != external_upgrade_receipt_digest(receipt) {
841 return Err(ExternalUpgradeReceiptError::DigestMismatch {
842 field: "receipt_digest",
843 });
844 }
845 Ok(())
846}
847
848pub fn validate_external_upgrade_receipt_for_proposal(
855 receipt: &ExternalUpgradeReceiptV1,
856 proposal: &ExternalUpgradeProposalV1,
857) -> Result<(), ExternalUpgradeReceiptError> {
858 validate_external_upgrade_receipt(receipt)?;
859 ensure_external_receipt_matches_proposal(
860 "proposal_id",
861 receipt.proposal_id.as_str(),
862 proposal.proposal_id.as_str(),
863 )?;
864 ensure_external_receipt_matches_proposal(
865 "proposal_digest",
866 receipt.proposal_digest.as_str(),
867 proposal.proposal_digest.as_str(),
868 )?;
869 ensure_external_receipt_matches_proposal(
870 "subject",
871 receipt.subject.as_str(),
872 proposal.subject.as_str(),
873 )?;
874 ensure_external_receipt_option_matches_proposal(
875 "canister_id",
876 receipt.canister_id.as_deref(),
877 proposal.canister_id.as_deref(),
878 )?;
879 ensure_external_receipt_option_matches_proposal(
880 "role",
881 receipt.role.as_deref(),
882 proposal.role.as_deref(),
883 )?;
884 ensure_external_receipt_option_matches_proposal(
885 "observed_before_module_hash",
886 receipt.observed_before_module_hash.as_deref(),
887 proposal.current_module_hash.as_deref(),
888 )?;
889
890 let expected_result = external_upgrade_verification_result(
891 receipt.consent_state,
892 proposal,
893 receipt.observed_after_module_hash.as_deref(),
894 receipt
895 .observed_after_canonical_embedded_config_sha256
896 .as_deref(),
897 );
898 if receipt.verification_result != expected_result {
899 return Err(ExternalUpgradeReceiptError::VerificationMismatch);
900 }
901 let expected_notes = external_upgrade_verification_notes(
902 expected_result,
903 proposal,
904 receipt.observed_after_module_hash.as_deref(),
905 receipt
906 .observed_after_canonical_embedded_config_sha256
907 .as_deref(),
908 );
909 if receipt.verification_notes != expected_notes {
910 return Err(ExternalUpgradeReceiptError::SourceMismatch {
911 field: "verification_notes",
912 });
913 }
914
915 Ok(())
916}
917
918pub fn external_upgrade_consent_evidence_from_receipt(
924 evidence_id: impl Into<String>,
925 proposal: &ExternalUpgradeProposalV1,
926 receipt: &ExternalUpgradeReceiptV1,
927) -> Result<ExternalUpgradeConsentEvidenceV1, ExternalUpgradeReceiptError> {
928 validate_external_upgrade_receipt_for_proposal(receipt, proposal)?;
929 let consent_state = receipt.consent_state;
930 let mut evidence = ExternalUpgradeConsentEvidenceV1 {
931 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
932 evidence_id: evidence_id.into(),
933 evidence_digest: String::new(),
934 proposal_id: proposal.proposal_id.clone(),
935 proposal_digest: proposal.proposal_digest.clone(),
936 receipt_id: receipt.receipt_id.clone(),
937 receipt_digest: receipt.receipt_digest.clone(),
938 subject: proposal.subject.clone(),
939 canister_id: proposal.canister_id.clone(),
940 role: proposal.role.clone(),
941 consent_state,
942 reported_by: receipt.reported_by.clone(),
943 consent_requirements: proposal.consent_requirements.clone(),
944 allowed_authorization_modes: proposal.allowed_authorization_modes.clone(),
945 status_summary: external_upgrade_consent_summary(consent_state).to_string(),
946 };
947 evidence.evidence_digest = external_upgrade_consent_evidence_digest(&evidence);
948 Ok(evidence)
949}
950
951pub fn validate_external_upgrade_consent_evidence(
953 evidence: &ExternalUpgradeConsentEvidenceV1,
954) -> Result<(), ExternalUpgradeConsentEvidenceError> {
955 if evidence.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
956 return Err(ExternalUpgradeConsentEvidenceError::SchemaVersionMismatch {
957 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
958 actual: evidence.schema_version,
959 });
960 }
961 ensure_external_consent_evidence_field("evidence_id", evidence.evidence_id.as_str())?;
962 ensure_external_consent_evidence_field("evidence_digest", evidence.evidence_digest.as_str())?;
963 ensure_external_consent_evidence_field("proposal_id", evidence.proposal_id.as_str())?;
964 ensure_external_consent_evidence_field("proposal_digest", evidence.proposal_digest.as_str())?;
965 ensure_external_consent_evidence_field("receipt_id", evidence.receipt_id.as_str())?;
966 ensure_external_consent_evidence_field("receipt_digest", evidence.receipt_digest.as_str())?;
967 ensure_external_consent_evidence_field("subject", evidence.subject.as_str())?;
968 ensure_external_consent_evidence_field("status_summary", evidence.status_summary.as_str())?;
969 if evidence.status_summary != external_upgrade_consent_summary(evidence.consent_state) {
970 return Err(ExternalUpgradeConsentEvidenceError::SourceMismatch {
971 field: "status_summary",
972 });
973 }
974 if evidence.evidence_digest != external_upgrade_consent_evidence_digest(evidence) {
975 return Err(ExternalUpgradeConsentEvidenceError::DigestMismatch {
976 field: "evidence_digest",
977 });
978 }
979 Ok(())
980}
981
982pub fn validate_external_upgrade_consent_evidence_for_receipt(
985 evidence: &ExternalUpgradeConsentEvidenceV1,
986 proposal: &ExternalUpgradeProposalV1,
987 receipt: &ExternalUpgradeReceiptV1,
988) -> Result<(), ExternalUpgradeConsentEvidenceError> {
989 validate_external_upgrade_consent_evidence(evidence)?;
990 let expected = external_upgrade_consent_evidence_from_receipt(
991 evidence.evidence_id.clone(),
992 proposal,
993 receipt,
994 )?;
995 if evidence != &expected {
996 return Err(ExternalUpgradeConsentEvidenceError::SourceMismatch { field: "receipt" });
997 }
998 Ok(())
999}
1000
1001pub fn external_upgrade_verification_report_from_receipt(
1006 report_id: impl Into<String>,
1007 proposal: &ExternalUpgradeProposalV1,
1008 receipt: &ExternalUpgradeReceiptV1,
1009) -> Result<ExternalUpgradeVerificationReportV1, ExternalUpgradeReceiptError> {
1010 validate_external_upgrade_receipt_for_proposal(receipt, proposal)?;
1011 let verification_result = receipt.verification_result;
1012 let mut report = ExternalUpgradeVerificationReportV1 {
1013 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1014 report_id: report_id.into(),
1015 report_digest: String::new(),
1016 proposal_id: proposal.proposal_id.clone(),
1017 proposal_digest: proposal.proposal_digest.clone(),
1018 receipt_id: receipt.receipt_id.clone(),
1019 receipt_digest: receipt.receipt_digest.clone(),
1020 subject: proposal.subject.clone(),
1021 canister_id: proposal.canister_id.clone(),
1022 role: proposal.role.clone(),
1023 verification_result,
1024 verification_notes: receipt.verification_notes.clone(),
1025 live_inventory_required: verification_result
1026 != ExternalUpgradeVerificationResultV1::Pending
1027 && verification_result != ExternalUpgradeVerificationResultV1::Refused,
1028 status_summary: external_upgrade_verification_summary(verification_result).to_string(),
1029 };
1030 report.report_digest = external_upgrade_verification_report_digest(&report);
1031 Ok(report)
1032}
1033
1034pub fn validate_external_upgrade_verification_report(
1037 report: &ExternalUpgradeVerificationReportV1,
1038) -> Result<(), ExternalUpgradeVerificationReportError> {
1039 if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1040 return Err(
1041 ExternalUpgradeVerificationReportError::SchemaVersionMismatch {
1042 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1043 actual: report.schema_version,
1044 },
1045 );
1046 }
1047 ensure_external_verification_report_field("report_id", report.report_id.as_str())?;
1048 ensure_external_verification_report_field("report_digest", report.report_digest.as_str())?;
1049 ensure_external_verification_report_field("proposal_id", report.proposal_id.as_str())?;
1050 ensure_external_verification_report_field("proposal_digest", report.proposal_digest.as_str())?;
1051 ensure_external_verification_report_field("receipt_id", report.receipt_id.as_str())?;
1052 ensure_external_verification_report_field("receipt_digest", report.receipt_digest.as_str())?;
1053 ensure_external_verification_report_field("subject", report.subject.as_str())?;
1054 ensure_external_verification_report_field("status_summary", report.status_summary.as_str())?;
1055 if report.status_summary != external_upgrade_verification_summary(report.verification_result) {
1056 return Err(ExternalUpgradeVerificationReportError::SourceMismatch {
1057 field: "status_summary",
1058 });
1059 }
1060 if report.report_digest != external_upgrade_verification_report_digest(report) {
1061 return Err(ExternalUpgradeVerificationReportError::DigestMismatch {
1062 field: "report_digest",
1063 });
1064 }
1065 Ok(())
1066}
1067
1068pub fn validate_external_upgrade_verification_report_for_receipt(
1071 report: &ExternalUpgradeVerificationReportV1,
1072 proposal: &ExternalUpgradeProposalV1,
1073 receipt: &ExternalUpgradeReceiptV1,
1074) -> Result<(), ExternalUpgradeVerificationReportError> {
1075 validate_external_upgrade_verification_report(report)?;
1076 let expected = external_upgrade_verification_report_from_receipt(
1077 report.report_id.clone(),
1078 proposal,
1079 receipt,
1080 )?;
1081 if report != &expected {
1082 return Err(ExternalUpgradeVerificationReportError::SourceMismatch { field: "receipt" });
1083 }
1084 Ok(())
1085}
1086
1087#[must_use]
1090pub fn external_upgrade_verification_policy_from_proposal(
1091 policy_id: impl Into<String>,
1092 proposal: &ExternalUpgradeProposalV1,
1093) -> ExternalUpgradeVerificationPolicyV1 {
1094 let mut policy = ExternalUpgradeVerificationPolicyV1 {
1095 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1096 policy_id: policy_id.into(),
1097 policy_digest: String::new(),
1098 proposal_id: proposal.proposal_id.clone(),
1099 proposal_digest: proposal.proposal_digest.clone(),
1100 subject: proposal.subject.clone(),
1101 canister_id: proposal.canister_id.clone(),
1102 role: proposal.role.clone(),
1103 required_verification: proposal.verification_requirements.clone(),
1104 verification_requirements: external_upgrade_verification_policy_requirements(proposal),
1105 max_observation_age_seconds: None,
1106 status_summary: external_upgrade_verification_policy_summary(proposal).to_string(),
1107 };
1108 policy.policy_digest = external_upgrade_verification_policy_digest(&policy);
1109 policy
1110}
1111
1112pub fn validate_external_upgrade_verification_policy(
1115 policy: &ExternalUpgradeVerificationPolicyV1,
1116) -> Result<(), ExternalUpgradeVerificationPolicyError> {
1117 if policy.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1118 return Err(
1119 ExternalUpgradeVerificationPolicyError::SchemaVersionMismatch {
1120 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1121 actual: policy.schema_version,
1122 },
1123 );
1124 }
1125 ensure_external_verification_policy_field("policy_id", policy.policy_id.as_str())?;
1126 ensure_external_verification_policy_field("policy_digest", policy.policy_digest.as_str())?;
1127 ensure_external_verification_policy_field("proposal_id", policy.proposal_id.as_str())?;
1128 ensure_external_verification_policy_field("proposal_digest", policy.proposal_digest.as_str())?;
1129 ensure_external_verification_policy_field("subject", policy.subject.as_str())?;
1130 ensure_external_verification_policy_field("status_summary", policy.status_summary.as_str())?;
1131 if policy.policy_digest != external_upgrade_verification_policy_digest(policy) {
1132 return Err(ExternalUpgradeVerificationPolicyError::DigestMismatch {
1133 field: "policy_digest",
1134 });
1135 }
1136 Ok(())
1137}
1138
1139pub fn validate_external_upgrade_verification_policy_for_proposal(
1142 policy: &ExternalUpgradeVerificationPolicyV1,
1143 proposal: &ExternalUpgradeProposalV1,
1144) -> Result<(), ExternalUpgradeVerificationPolicyError> {
1145 validate_external_upgrade_verification_policy(policy)?;
1146 let expected =
1147 external_upgrade_verification_policy_from_proposal(policy.policy_id.clone(), proposal);
1148 if policy != &expected {
1149 return Err(ExternalUpgradeVerificationPolicyError::SourceMismatch { field: "proposal" });
1150 }
1151 Ok(())
1152}
1153
1154#[must_use]
1159pub fn external_upgrade_verification_check_from_policy(
1160 check_id: impl Into<String>,
1161 policy: &ExternalUpgradeVerificationPolicyV1,
1162 observation: ExternalUpgradeVerificationObservationV1,
1163) -> ExternalUpgradeVerificationCheckV1 {
1164 let requirement_results =
1165 external_upgrade_verification_check_requirements(policy, &observation);
1166 let verification_result = external_upgrade_verification_check_result(&requirement_results);
1167 let mut check = ExternalUpgradeVerificationCheckV1 {
1168 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1169 check_id: check_id.into(),
1170 check_digest: String::new(),
1171 policy_id: policy.policy_id.clone(),
1172 policy_digest: policy.policy_digest.clone(),
1173 proposal_id: policy.proposal_id.clone(),
1174 proposal_digest: policy.proposal_digest.clone(),
1175 subject: policy.subject.clone(),
1176 canister_id: policy.canister_id.clone(),
1177 role: policy.role.clone(),
1178 observation,
1179 requirement_results,
1180 verification_result,
1181 status_summary: external_upgrade_verification_check_summary(verification_result)
1182 .to_string(),
1183 };
1184 check.check_digest = external_upgrade_verification_check_digest(&check);
1185 check
1186}
1187
1188pub fn validate_external_upgrade_verification_check(
1191 check: &ExternalUpgradeVerificationCheckV1,
1192) -> Result<(), ExternalUpgradeVerificationCheckError> {
1193 if check.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1194 return Err(
1195 ExternalUpgradeVerificationCheckError::SchemaVersionMismatch {
1196 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1197 actual: check.schema_version,
1198 },
1199 );
1200 }
1201 ensure_external_verification_check_field("check_id", check.check_id.as_str())?;
1202 ensure_external_verification_check_field("check_digest", check.check_digest.as_str())?;
1203 ensure_external_verification_check_field("policy_id", check.policy_id.as_str())?;
1204 ensure_external_verification_check_field("policy_digest", check.policy_digest.as_str())?;
1205 ensure_external_verification_check_field("proposal_id", check.proposal_id.as_str())?;
1206 ensure_external_verification_check_field("proposal_digest", check.proposal_digest.as_str())?;
1207 ensure_external_verification_check_field("subject", check.subject.as_str())?;
1208 ensure_external_verification_check_field("status_summary", check.status_summary.as_str())?;
1209 validate_external_upgrade_verification_check_requirements(
1210 &check.requirement_results,
1211 check.verification_result,
1212 )?;
1213 if check.status_summary
1214 != external_upgrade_verification_check_summary(check.verification_result)
1215 {
1216 return Err(ExternalUpgradeVerificationCheckError::SourceMismatch {
1217 field: "status_summary",
1218 });
1219 }
1220 if check.check_digest != external_upgrade_verification_check_digest(check) {
1221 return Err(ExternalUpgradeVerificationCheckError::DigestMismatch {
1222 field: "check_digest",
1223 });
1224 }
1225 Ok(())
1226}
1227
1228pub fn validate_external_upgrade_verification_check_for_policy(
1231 check: &ExternalUpgradeVerificationCheckV1,
1232 policy: &ExternalUpgradeVerificationPolicyV1,
1233) -> Result<(), ExternalUpgradeVerificationCheckError> {
1234 validate_external_upgrade_verification_check(check)?;
1235 let expected = external_upgrade_verification_check_from_policy(
1236 check.check_id.clone(),
1237 policy,
1238 check.observation.clone(),
1239 );
1240 if check != &expected {
1241 return Err(ExternalUpgradeVerificationCheckError::SourceMismatch { field: "policy" });
1242 }
1243 Ok(())
1244}
1245
1246pub fn external_upgrade_completion_report_from_evidence(
1251 report_id: impl Into<String>,
1252 proposal: &ExternalUpgradeProposalV1,
1253 consent_evidence: &ExternalUpgradeConsentEvidenceV1,
1254 verification_check: &ExternalUpgradeVerificationCheckV1,
1255) -> Result<ExternalUpgradeCompletionReportV1, ExternalUpgradeCompletionReportError> {
1256 validate_external_upgrade_proposal(proposal)?;
1257 validate_external_upgrade_consent_evidence(consent_evidence)?;
1258 validate_external_upgrade_verification_check(verification_check)?;
1259 ensure_completion_sources_match_proposal(proposal, consent_evidence, verification_check)?;
1260
1261 let completion_status = external_upgrade_completion_status(
1262 consent_evidence.consent_state,
1263 verification_check.verification_result,
1264 );
1265 let blockers = external_upgrade_completion_blockers(completion_status);
1266 let next_actions = external_upgrade_completion_next_actions(completion_status);
1267 let mut report = ExternalUpgradeCompletionReportV1 {
1268 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1269 report_id: report_id.into(),
1270 report_digest: String::new(),
1271 proposal_id: proposal.proposal_id.clone(),
1272 proposal_digest: proposal.proposal_digest.clone(),
1273 consent_evidence_id: consent_evidence.evidence_id.clone(),
1274 consent_evidence_digest: consent_evidence.evidence_digest.clone(),
1275 verification_check_id: verification_check.check_id.clone(),
1276 verification_check_digest: verification_check.check_digest.clone(),
1277 subject: proposal.subject.clone(),
1278 canister_id: proposal.canister_id.clone(),
1279 role: proposal.role.clone(),
1280 consent_state: consent_evidence.consent_state,
1281 verification_result: verification_check.verification_result,
1282 completion_status,
1283 blockers,
1284 next_actions,
1285 status_summary: external_upgrade_completion_summary(completion_status).to_string(),
1286 };
1287 report.report_digest = external_upgrade_completion_report_digest(&report);
1288 Ok(report)
1289}
1290
1291pub fn validate_external_upgrade_completion_report(
1293 report: &ExternalUpgradeCompletionReportV1,
1294) -> Result<(), ExternalUpgradeCompletionReportError> {
1295 if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1296 return Err(
1297 ExternalUpgradeCompletionReportError::SchemaVersionMismatch {
1298 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1299 actual: report.schema_version,
1300 },
1301 );
1302 }
1303 ensure_external_completion_report_field("report_id", report.report_id.as_str())?;
1304 ensure_external_completion_report_field("report_digest", report.report_digest.as_str())?;
1305 ensure_external_completion_report_field("proposal_id", report.proposal_id.as_str())?;
1306 ensure_external_completion_report_field("proposal_digest", report.proposal_digest.as_str())?;
1307 ensure_external_completion_report_field(
1308 "consent_evidence_id",
1309 report.consent_evidence_id.as_str(),
1310 )?;
1311 ensure_external_completion_report_field(
1312 "consent_evidence_digest",
1313 report.consent_evidence_digest.as_str(),
1314 )?;
1315 ensure_external_completion_report_field(
1316 "verification_check_id",
1317 report.verification_check_id.as_str(),
1318 )?;
1319 ensure_external_completion_report_field(
1320 "verification_check_digest",
1321 report.verification_check_digest.as_str(),
1322 )?;
1323 ensure_external_completion_report_field("subject", report.subject.as_str())?;
1324 ensure_external_completion_report_field("status_summary", report.status_summary.as_str())?;
1325 if report.completion_status
1326 != external_upgrade_completion_status(report.consent_state, report.verification_result)
1327 {
1328 return Err(ExternalUpgradeCompletionReportError::SourceMismatch {
1329 field: "completion_status",
1330 });
1331 }
1332 if report.status_summary != external_upgrade_completion_summary(report.completion_status) {
1333 return Err(ExternalUpgradeCompletionReportError::SourceMismatch {
1334 field: "status_summary",
1335 });
1336 }
1337 if report.blockers != external_upgrade_completion_blockers(report.completion_status) {
1338 return Err(ExternalUpgradeCompletionReportError::SourceMismatch { field: "blockers" });
1339 }
1340 if report.next_actions != external_upgrade_completion_next_actions(report.completion_status) {
1341 return Err(ExternalUpgradeCompletionReportError::SourceMismatch {
1342 field: "next_actions",
1343 });
1344 }
1345 if report.report_digest != external_upgrade_completion_report_digest(report) {
1346 return Err(ExternalUpgradeCompletionReportError::DigestMismatch {
1347 field: "report_digest",
1348 });
1349 }
1350 Ok(())
1351}
1352
1353pub fn validate_external_upgrade_completion_report_for_evidence(
1356 report: &ExternalUpgradeCompletionReportV1,
1357 proposal: &ExternalUpgradeProposalV1,
1358 consent_evidence: &ExternalUpgradeConsentEvidenceV1,
1359 verification_check: &ExternalUpgradeVerificationCheckV1,
1360) -> Result<(), ExternalUpgradeCompletionReportError> {
1361 validate_external_upgrade_completion_report(report)?;
1362 let expected = external_upgrade_completion_report_from_evidence(
1363 report.report_id.clone(),
1364 proposal,
1365 consent_evidence,
1366 verification_check,
1367 )?;
1368 if report != &expected {
1369 return Err(ExternalUpgradeCompletionReportError::SourceMismatch {
1370 field: "source_evidence",
1371 });
1372 }
1373 Ok(())
1374}
1375
1376#[must_use]
1381pub fn external_upgrade_proposal_report_from_lifecycle_plan(
1382 report_id: impl Into<String>,
1383 lifecycle_plan: &ExternalLifecyclePlanV1,
1384 check: &DeploymentCheckV1,
1385) -> ExternalUpgradeProposalReportV1 {
1386 let report_id = report_id.into();
1387 let mut proposals = Vec::new();
1388 for authority in lifecycle_plan
1389 .lifecycle_authority_rows
1390 .iter()
1391 .filter(|authority| authority.external_action_required && !authority.blocked)
1392 {
1393 proposals.push(external_upgrade_proposal(
1394 &report_id,
1395 lifecycle_plan,
1396 check,
1397 authority,
1398 observed_canister_for_authority(&check.inventory, authority),
1399 target_artifact_for_authority(&check.plan, authority),
1400 ));
1401 }
1402
1403 proposals.sort_by(|left, right| left.subject.cmp(&right.subject));
1404
1405 let mut report = ExternalUpgradeProposalReportV1 {
1406 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1407 report_id,
1408 report_digest: String::new(),
1409 lifecycle_plan_id: lifecycle_plan.lifecycle_plan_id.clone(),
1410 lifecycle_plan_digest: lifecycle_plan.lifecycle_plan_digest.clone(),
1411 deployment_plan_id: lifecycle_plan.deployment_plan_id.clone(),
1412 deployment_plan_digest: lifecycle_plan.deployment_plan_digest.clone(),
1413 inventory_id: check.inventory.inventory_id.clone(),
1414 proposals,
1415 blocked_subjects: lifecycle_plan
1416 .blocked_role_upgrades
1417 .iter()
1418 .map(|upgrade| upgrade.subject.clone())
1419 .collect(),
1420 };
1421 report.report_digest = external_upgrade_proposal_report_digest(&report);
1422 report
1423}
1424
1425pub fn validate_external_upgrade_proposal_report(
1427 report: &ExternalUpgradeProposalReportV1,
1428) -> Result<(), ExternalUpgradeProposalReportError> {
1429 if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1430 return Err(ExternalUpgradeProposalReportError::SchemaVersionMismatch {
1431 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1432 actual: report.schema_version,
1433 });
1434 }
1435 ensure_external_proposal_report_field("report_id", report.report_id.as_str())?;
1436 ensure_external_proposal_report_field("report_digest", report.report_digest.as_str())?;
1437 ensure_external_proposal_report_field("lifecycle_plan_id", report.lifecycle_plan_id.as_str())?;
1438 ensure_external_proposal_report_field(
1439 "lifecycle_plan_digest",
1440 report.lifecycle_plan_digest.as_str(),
1441 )?;
1442 ensure_external_proposal_report_field(
1443 "deployment_plan_id",
1444 report.deployment_plan_id.as_str(),
1445 )?;
1446 ensure_external_proposal_report_field(
1447 "deployment_plan_digest",
1448 report.deployment_plan_digest.as_str(),
1449 )?;
1450 ensure_external_proposal_report_field("inventory_id", report.inventory_id.as_str())?;
1451
1452 let mut subjects = BTreeSet::new();
1453 for proposal in &report.proposals {
1454 if !subjects.insert(proposal.subject.clone()) {
1455 return Err(ExternalUpgradeProposalReportError::DuplicateSubject {
1456 subject: proposal.subject.clone(),
1457 });
1458 }
1459 validate_external_upgrade_proposal(proposal)?;
1460 }
1461 if report.report_digest != external_upgrade_proposal_report_digest(report) {
1462 return Err(ExternalUpgradeProposalReportError::DigestMismatch {
1463 field: "report_digest",
1464 });
1465 }
1466 Ok(())
1467}
1468
1469pub fn validate_external_upgrade_proposal_report_for_lifecycle_plan(
1472 report: &ExternalUpgradeProposalReportV1,
1473 lifecycle_plan: &ExternalLifecyclePlanV1,
1474 check: &DeploymentCheckV1,
1475) -> Result<(), ExternalUpgradeProposalReportError> {
1476 validate_external_upgrade_proposal_report(report)?;
1477 if report.lifecycle_plan_id != lifecycle_plan.lifecycle_plan_id {
1478 return Err(ExternalUpgradeProposalReportError::SourceMismatch {
1479 field: "lifecycle_plan_id",
1480 });
1481 }
1482 if report.lifecycle_plan_digest != lifecycle_plan.lifecycle_plan_digest {
1483 return Err(ExternalUpgradeProposalReportError::SourceMismatch {
1484 field: "lifecycle_plan_digest",
1485 });
1486 }
1487 let expected = external_upgrade_proposal_report_from_lifecycle_plan(
1488 report.report_id.clone(),
1489 lifecycle_plan,
1490 check,
1491 );
1492 if report != &expected {
1493 return Err(ExternalUpgradeProposalReportError::SourceMismatch {
1494 field: "deployment_check",
1495 });
1496 }
1497 Ok(())
1498}
1499
1500#[must_use]
1503pub fn external_lifecycle_pending_report_from_plan(
1504 report_id: impl Into<String>,
1505 lifecycle_plan: &ExternalLifecyclePlanV1,
1506 proposal_report: &ExternalUpgradeProposalReportV1,
1507) -> ExternalLifecyclePendingReportV1 {
1508 let report_id = report_id.into();
1509 let pending_external_actions = proposal_report
1510 .proposals
1511 .iter()
1512 .map(external_lifecycle_pending_action)
1513 .collect::<Vec<_>>();
1514 let blocked_subjects = lifecycle_plan
1515 .blocked_role_upgrades
1516 .iter()
1517 .map(|upgrade| upgrade.subject.clone())
1518 .collect::<Vec<_>>();
1519 let mut report = ExternalLifecyclePendingReportV1 {
1520 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1521 report_id,
1522 report_digest: String::new(),
1523 lifecycle_plan_id: lifecycle_plan.lifecycle_plan_id.clone(),
1524 lifecycle_plan_digest: lifecycle_plan.lifecycle_plan_digest.clone(),
1525 proposal_report_id: proposal_report.report_id.clone(),
1526 proposal_report_digest: proposal_report.report_digest.clone(),
1527 deployment_plan_id: lifecycle_plan.deployment_plan_id.clone(),
1528 deployment_plan_digest: lifecycle_plan.deployment_plan_digest.clone(),
1529 inventory_id: lifecycle_plan.inventory_id.clone(),
1530 direct_upgrade_count: lifecycle_plan.directly_executable_role_upgrades.len(),
1531 pending_external_count: pending_external_actions.len(),
1532 blocked_count: blocked_subjects.len(),
1533 pending_external_actions,
1534 blocked_subjects,
1535 residual_exposure: lifecycle_plan.residual_exposure.clone(),
1536 status: lifecycle_plan.status,
1537 };
1538 report.report_digest = external_lifecycle_pending_report_digest(&report);
1539 report
1540}
1541
1542pub fn validate_external_lifecycle_pending_report(
1544 report: &ExternalLifecyclePendingReportV1,
1545) -> Result<(), ExternalLifecyclePendingReportError> {
1546 if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1547 return Err(ExternalLifecyclePendingReportError::SchemaVersionMismatch {
1548 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1549 actual: report.schema_version,
1550 });
1551 }
1552 ensure_external_pending_report_field("report_id", report.report_id.as_str())?;
1553 ensure_external_pending_report_field("report_digest", report.report_digest.as_str())?;
1554 ensure_external_pending_report_field("lifecycle_plan_id", report.lifecycle_plan_id.as_str())?;
1555 ensure_external_pending_report_field(
1556 "lifecycle_plan_digest",
1557 report.lifecycle_plan_digest.as_str(),
1558 )?;
1559 ensure_external_pending_report_field("proposal_report_id", report.proposal_report_id.as_str())?;
1560 ensure_external_pending_report_field(
1561 "proposal_report_digest",
1562 report.proposal_report_digest.as_str(),
1563 )?;
1564 ensure_external_pending_report_field("deployment_plan_id", report.deployment_plan_id.as_str())?;
1565 ensure_external_pending_report_field(
1566 "deployment_plan_digest",
1567 report.deployment_plan_digest.as_str(),
1568 )?;
1569 ensure_external_pending_report_field("inventory_id", report.inventory_id.as_str())?;
1570 if report.pending_external_count != report.pending_external_actions.len()
1571 || report.blocked_count != report.blocked_subjects.len()
1572 {
1573 return Err(ExternalLifecyclePendingReportError::CountMismatch);
1574 }
1575 let mut subjects = BTreeSet::new();
1576 for action in &report.pending_external_actions {
1577 ensure_external_pending_report_field("pending_action.subject", action.subject.as_str())?;
1578 ensure_external_pending_report_field(
1579 "pending_action.proposal_id",
1580 action.proposal_id.as_str(),
1581 )?;
1582 ensure_external_pending_report_field(
1583 "pending_action.proposal_digest",
1584 action.proposal_digest.as_str(),
1585 )?;
1586 ensure_external_pending_report_field(
1587 "pending_action.required_external_action",
1588 action.required_external_action.as_str(),
1589 )?;
1590 if !subjects.insert(action.subject.clone()) {
1591 return Err(ExternalLifecyclePendingReportError::DuplicateSubject {
1592 subject: action.subject.clone(),
1593 });
1594 }
1595 }
1596 if report.report_digest != external_lifecycle_pending_report_digest(report) {
1597 return Err(ExternalLifecyclePendingReportError::DigestMismatch {
1598 field: "report_digest",
1599 });
1600 }
1601 Ok(())
1602}
1603
1604pub fn validate_external_lifecycle_pending_report_for_plan(
1607 report: &ExternalLifecyclePendingReportV1,
1608 lifecycle_plan: &ExternalLifecyclePlanV1,
1609 proposal_report: &ExternalUpgradeProposalReportV1,
1610) -> Result<(), ExternalLifecyclePendingReportError> {
1611 validate_external_lifecycle_pending_report(report)?;
1612 if report.lifecycle_plan_id != lifecycle_plan.lifecycle_plan_id {
1613 return Err(ExternalLifecyclePendingReportError::SourceMismatch {
1614 field: "lifecycle_plan_id",
1615 });
1616 }
1617 if report.lifecycle_plan_digest != lifecycle_plan.lifecycle_plan_digest {
1618 return Err(ExternalLifecyclePendingReportError::SourceMismatch {
1619 field: "lifecycle_plan_digest",
1620 });
1621 }
1622 if report.proposal_report_id != proposal_report.report_id {
1623 return Err(ExternalLifecyclePendingReportError::SourceMismatch {
1624 field: "proposal_report_id",
1625 });
1626 }
1627 if report.proposal_report_digest != proposal_report.report_digest {
1628 return Err(ExternalLifecyclePendingReportError::SourceMismatch {
1629 field: "proposal_report_digest",
1630 });
1631 }
1632 let expected = external_lifecycle_pending_report_from_plan(
1633 report.report_id.clone(),
1634 lifecycle_plan,
1635 proposal_report,
1636 );
1637 if report != &expected {
1638 return Err(ExternalLifecyclePendingReportError::SourceMismatch {
1639 field: "lifecycle_plan",
1640 });
1641 }
1642 Ok(())
1643}
1644
1645#[must_use]
1647pub fn external_lifecycle_check_from_reports(
1648 check_id: impl Into<String>,
1649 lifecycle_plan: &ExternalLifecyclePlanV1,
1650 proposal_report: &ExternalUpgradeProposalReportV1,
1651 pending_report: &ExternalLifecyclePendingReportV1,
1652) -> ExternalLifecycleCheckV1 {
1653 let check_id = check_id.into();
1654 let status = pending_report.status;
1655 let mut check = ExternalLifecycleCheckV1 {
1656 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1657 check_id,
1658 check_digest: String::new(),
1659 lifecycle_plan_id: lifecycle_plan.lifecycle_plan_id.clone(),
1660 lifecycle_plan_digest: lifecycle_plan.lifecycle_plan_digest.clone(),
1661 proposal_report_id: proposal_report.report_id.clone(),
1662 proposal_report_digest: proposal_report.report_digest.clone(),
1663 pending_report_id: pending_report.report_id.clone(),
1664 pending_report_digest: pending_report.report_digest.clone(),
1665 deployment_plan_id: lifecycle_plan.deployment_plan_id.clone(),
1666 deployment_plan_digest: lifecycle_plan.deployment_plan_digest.clone(),
1667 inventory_id: lifecycle_plan.inventory_id.clone(),
1668 status,
1669 direct_upgrade_count: pending_report.direct_upgrade_count,
1670 pending_external_count: pending_report.pending_external_count,
1671 blocked_count: pending_report.blocked_count,
1672 residual_exposure_count: pending_report.residual_exposure.len(),
1673 summary: external_lifecycle_check_summary(status, pending_report),
1674 next_actions: external_lifecycle_check_next_actions(status, pending_report),
1675 };
1676 check.check_digest = external_lifecycle_check_digest(&check);
1677 check
1678}
1679
1680pub fn validate_external_lifecycle_check(
1682 check: &ExternalLifecycleCheckV1,
1683) -> Result<(), ExternalLifecycleCheckError> {
1684 if check.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1685 return Err(ExternalLifecycleCheckError::SchemaVersionMismatch {
1686 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1687 actual: check.schema_version,
1688 });
1689 }
1690 ensure_external_lifecycle_check_field("check_id", check.check_id.as_str())?;
1691 ensure_external_lifecycle_check_field("check_digest", check.check_digest.as_str())?;
1692 ensure_external_lifecycle_check_field("lifecycle_plan_id", check.lifecycle_plan_id.as_str())?;
1693 ensure_external_lifecycle_check_field(
1694 "lifecycle_plan_digest",
1695 check.lifecycle_plan_digest.as_str(),
1696 )?;
1697 ensure_external_lifecycle_check_field("proposal_report_id", check.proposal_report_id.as_str())?;
1698 ensure_external_lifecycle_check_field(
1699 "proposal_report_digest",
1700 check.proposal_report_digest.as_str(),
1701 )?;
1702 ensure_external_lifecycle_check_field("pending_report_id", check.pending_report_id.as_str())?;
1703 ensure_external_lifecycle_check_field(
1704 "pending_report_digest",
1705 check.pending_report_digest.as_str(),
1706 )?;
1707 ensure_external_lifecycle_check_field("deployment_plan_id", check.deployment_plan_id.as_str())?;
1708 ensure_external_lifecycle_check_field(
1709 "deployment_plan_digest",
1710 check.deployment_plan_digest.as_str(),
1711 )?;
1712 ensure_external_lifecycle_check_field("inventory_id", check.inventory_id.as_str())?;
1713 ensure_external_lifecycle_check_field("summary", check.summary.as_str())?;
1714 if check.check_digest != external_lifecycle_check_digest(check) {
1715 return Err(ExternalLifecycleCheckError::DigestMismatch {
1716 field: "check_digest",
1717 });
1718 }
1719 Ok(())
1720}
1721
1722pub fn validate_external_lifecycle_check_for_reports(
1725 check: &ExternalLifecycleCheckV1,
1726 lifecycle_plan: &ExternalLifecyclePlanV1,
1727 proposal_report: &ExternalUpgradeProposalReportV1,
1728 pending_report: &ExternalLifecyclePendingReportV1,
1729) -> Result<(), ExternalLifecycleCheckError> {
1730 validate_external_lifecycle_check(check)?;
1731 if check.lifecycle_plan_id != lifecycle_plan.lifecycle_plan_id {
1732 return Err(ExternalLifecycleCheckError::SourceMismatch {
1733 field: "lifecycle_plan_id",
1734 });
1735 }
1736 if check.lifecycle_plan_digest != lifecycle_plan.lifecycle_plan_digest {
1737 return Err(ExternalLifecycleCheckError::SourceMismatch {
1738 field: "lifecycle_plan_digest",
1739 });
1740 }
1741 if check.proposal_report_id != proposal_report.report_id {
1742 return Err(ExternalLifecycleCheckError::SourceMismatch {
1743 field: "proposal_report_id",
1744 });
1745 }
1746 if check.proposal_report_digest != proposal_report.report_digest {
1747 return Err(ExternalLifecycleCheckError::SourceMismatch {
1748 field: "proposal_report_digest",
1749 });
1750 }
1751 if check.pending_report_id != pending_report.report_id {
1752 return Err(ExternalLifecycleCheckError::SourceMismatch {
1753 field: "pending_report_id",
1754 });
1755 }
1756 if check.pending_report_digest != pending_report.report_digest {
1757 return Err(ExternalLifecycleCheckError::SourceMismatch {
1758 field: "pending_report_digest",
1759 });
1760 }
1761 if check.direct_upgrade_count != pending_report.direct_upgrade_count
1762 || check.pending_external_count != pending_report.pending_external_count
1763 || check.blocked_count != pending_report.blocked_count
1764 || check.residual_exposure_count != pending_report.residual_exposure.len()
1765 {
1766 return Err(ExternalLifecycleCheckError::CountMismatch);
1767 }
1768 let expected = external_lifecycle_check_from_reports(
1769 check.check_id.clone(),
1770 lifecycle_plan,
1771 proposal_report,
1772 pending_report,
1773 );
1774 if check != &expected {
1775 return Err(ExternalLifecycleCheckError::SourceMismatch {
1776 field: "pending_report",
1777 });
1778 }
1779 Ok(())
1780}
1781
1782#[must_use]
1784pub fn external_lifecycle_handoff_from_reports(
1785 handoff_id: impl Into<String>,
1786 lifecycle_check: &ExternalLifecycleCheckV1,
1787 proposal_report: &ExternalUpgradeProposalReportV1,
1788 pending_report: &ExternalLifecyclePendingReportV1,
1789) -> ExternalLifecycleHandoffV1 {
1790 let proposal_by_id = proposal_report
1791 .proposals
1792 .iter()
1793 .map(|proposal| (proposal.proposal_id.as_str(), proposal))
1794 .collect::<BTreeMap<_, _>>();
1795 let handoff_actions = pending_report
1796 .pending_external_actions
1797 .iter()
1798 .filter_map(|action| proposal_by_id.get(action.proposal_id.as_str()))
1799 .map(|proposal| external_lifecycle_handoff_action(proposal))
1800 .collect::<Vec<_>>();
1801 let mut handoff = ExternalLifecycleHandoffV1 {
1802 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1803 handoff_id: handoff_id.into(),
1804 handoff_digest: String::new(),
1805 lifecycle_check_id: lifecycle_check.check_id.clone(),
1806 lifecycle_check_digest: lifecycle_check.check_digest.clone(),
1807 pending_report_id: pending_report.report_id.clone(),
1808 pending_report_digest: pending_report.report_digest.clone(),
1809 proposal_report_id: proposal_report.report_id.clone(),
1810 proposal_report_digest: proposal_report.report_digest.clone(),
1811 deployment_plan_id: pending_report.deployment_plan_id.clone(),
1812 deployment_plan_digest: pending_report.deployment_plan_digest.clone(),
1813 inventory_id: pending_report.inventory_id.clone(),
1814 status: pending_report.status,
1815 handoff_actions,
1816 blocked_subjects: pending_report.blocked_subjects.clone(),
1817 residual_exposure: pending_report.residual_exposure.clone(),
1818 operator_summary: external_lifecycle_handoff_summary(pending_report),
1819 };
1820 handoff.handoff_digest = external_lifecycle_handoff_digest(&handoff);
1821 handoff
1822}
1823
1824pub fn validate_external_lifecycle_handoff(
1826 handoff: &ExternalLifecycleHandoffV1,
1827) -> Result<(), ExternalLifecycleHandoffError> {
1828 if handoff.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1829 return Err(ExternalLifecycleHandoffError::SchemaVersionMismatch {
1830 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1831 actual: handoff.schema_version,
1832 });
1833 }
1834 ensure_external_lifecycle_handoff_field("handoff_id", handoff.handoff_id.as_str())?;
1835 ensure_external_lifecycle_handoff_field("handoff_digest", handoff.handoff_digest.as_str())?;
1836 ensure_external_lifecycle_handoff_field(
1837 "lifecycle_check_id",
1838 handoff.lifecycle_check_id.as_str(),
1839 )?;
1840 ensure_external_lifecycle_handoff_field(
1841 "lifecycle_check_digest",
1842 handoff.lifecycle_check_digest.as_str(),
1843 )?;
1844 ensure_external_lifecycle_handoff_field(
1845 "pending_report_id",
1846 handoff.pending_report_id.as_str(),
1847 )?;
1848 ensure_external_lifecycle_handoff_field(
1849 "pending_report_digest",
1850 handoff.pending_report_digest.as_str(),
1851 )?;
1852 ensure_external_lifecycle_handoff_field(
1853 "proposal_report_id",
1854 handoff.proposal_report_id.as_str(),
1855 )?;
1856 ensure_external_lifecycle_handoff_field(
1857 "proposal_report_digest",
1858 handoff.proposal_report_digest.as_str(),
1859 )?;
1860 ensure_external_lifecycle_handoff_field(
1861 "deployment_plan_id",
1862 handoff.deployment_plan_id.as_str(),
1863 )?;
1864 ensure_external_lifecycle_handoff_field(
1865 "deployment_plan_digest",
1866 handoff.deployment_plan_digest.as_str(),
1867 )?;
1868 ensure_external_lifecycle_handoff_field("inventory_id", handoff.inventory_id.as_str())?;
1869 ensure_external_lifecycle_handoff_field("operator_summary", handoff.operator_summary.as_str())?;
1870 let mut subjects = BTreeSet::new();
1871 for action in &handoff.handoff_actions {
1872 ensure_external_lifecycle_handoff_field("handoff_action.subject", action.subject.as_str())?;
1873 ensure_external_lifecycle_handoff_field(
1874 "handoff_action.proposal_id",
1875 action.proposal_id.as_str(),
1876 )?;
1877 ensure_external_lifecycle_handoff_field(
1878 "handoff_action.proposal_digest",
1879 action.proposal_digest.as_str(),
1880 )?;
1881 ensure_external_lifecycle_handoff_field(
1882 "handoff_action.required_external_action",
1883 action.required_external_action.as_str(),
1884 )?;
1885 if !subjects.insert(action.subject.clone()) {
1886 return Err(ExternalLifecycleHandoffError::DuplicateSubject {
1887 subject: action.subject.clone(),
1888 });
1889 }
1890 }
1891 if handoff.handoff_digest != external_lifecycle_handoff_digest(handoff) {
1892 return Err(ExternalLifecycleHandoffError::DigestMismatch {
1893 field: "handoff_digest",
1894 });
1895 }
1896 Ok(())
1897}
1898
1899pub fn validate_external_lifecycle_handoff_for_reports(
1902 handoff: &ExternalLifecycleHandoffV1,
1903 lifecycle_check: &ExternalLifecycleCheckV1,
1904 proposal_report: &ExternalUpgradeProposalReportV1,
1905 pending_report: &ExternalLifecyclePendingReportV1,
1906) -> Result<(), ExternalLifecycleHandoffError> {
1907 validate_external_lifecycle_handoff(handoff)?;
1908 if handoff.lifecycle_check_id != lifecycle_check.check_id {
1909 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1910 field: "lifecycle_check_id",
1911 });
1912 }
1913 if handoff.lifecycle_check_digest != lifecycle_check.check_digest {
1914 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1915 field: "lifecycle_check_digest",
1916 });
1917 }
1918 if handoff.pending_report_id != pending_report.report_id {
1919 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1920 field: "pending_report_id",
1921 });
1922 }
1923 if handoff.pending_report_digest != pending_report.report_digest {
1924 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1925 field: "pending_report_digest",
1926 });
1927 }
1928 if handoff.proposal_report_id != proposal_report.report_id {
1929 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1930 field: "proposal_report_id",
1931 });
1932 }
1933 if handoff.proposal_report_digest != proposal_report.report_digest {
1934 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1935 field: "proposal_report_digest",
1936 });
1937 }
1938 let expected = external_lifecycle_handoff_from_reports(
1939 handoff.handoff_id.clone(),
1940 lifecycle_check,
1941 proposal_report,
1942 pending_report,
1943 );
1944 if handoff != &expected {
1945 return Err(ExternalLifecycleHandoffError::SourceMismatch {
1946 field: "pending_report",
1947 });
1948 }
1949 Ok(())
1950}
1951
1952fn external_lifecycle_pending_action(
1953 proposal: &ExternalUpgradeProposalV1,
1954) -> ExternalLifecyclePendingActionV1 {
1955 ExternalLifecyclePendingActionV1 {
1956 subject: proposal.subject.clone(),
1957 proposal_id: proposal.proposal_id.clone(),
1958 proposal_digest: proposal.proposal_digest.clone(),
1959 canister_id: proposal.canister_id.clone(),
1960 role: proposal.role.clone(),
1961 control_class: proposal.control_class,
1962 lifecycle_mode: proposal.lifecycle_mode,
1963 required_external_action: proposal.required_external_action.clone(),
1964 consent_requirements: proposal.consent_requirements.clone(),
1965 verification_requirements: proposal.verification_requirements.clone(),
1966 }
1967}
1968
1969fn external_lifecycle_handoff_action(
1970 proposal: &ExternalUpgradeProposalV1,
1971) -> ExternalLifecycleHandoffActionV1 {
1972 let primary_requirement = proposal.consent_requirements.first();
1973 ExternalLifecycleHandoffActionV1 {
1974 subject: proposal.subject.clone(),
1975 proposal_id: proposal.proposal_id.clone(),
1976 proposal_digest: proposal.proposal_digest.clone(),
1977 canister_id: proposal.canister_id.clone(),
1978 role: proposal.role.clone(),
1979 control_class: proposal.control_class,
1980 lifecycle_mode: proposal.lifecycle_mode,
1981 required_external_action: proposal.required_external_action.clone(),
1982 consent_channel_kind: primary_requirement
1983 .map_or(ConsentChannelKindV1::OutOfBand, |requirement| {
1984 requirement.consent_channel_kind
1985 }),
1986 consent_subject_kind: primary_requirement.map_or(
1987 ConsentSubjectKindV1::UnknownExternalController,
1988 |requirement| requirement.consent_subject_kind,
1989 ),
1990 required_principals: primary_requirement.map_or_else(Vec::new, |requirement| {
1991 requirement.required_principals.clone()
1992 }),
1993 current_module_hash: proposal.current_module_hash.clone(),
1994 target_installed_module_hash: proposal.target_installed_module_hash.clone(),
1995 target_canonical_embedded_config_sha256: proposal
1996 .target_canonical_embedded_config_sha256
1997 .clone(),
1998 verification_requirements: proposal.verification_requirements.clone(),
1999 operator_instructions: external_lifecycle_handoff_instructions(proposal),
2000 }
2001}
2002
2003fn lifecycle_roles(lifecycle_plan: &ExternalLifecyclePlanV1) -> Vec<String> {
2004 lifecycle_plan
2005 .lifecycle_authority_rows
2006 .iter()
2007 .filter_map(|authority| authority.role.clone())
2008 .collect::<BTreeSet<_>>()
2009 .into_iter()
2010 .collect()
2011}
2012
2013fn lifecycle_canisters(lifecycle_plan: &ExternalLifecyclePlanV1) -> Vec<String> {
2014 lifecycle_plan
2015 .lifecycle_authority_rows
2016 .iter()
2017 .filter_map(|authority| authority.canister_id.clone())
2018 .collect::<BTreeSet<_>>()
2019 .into_iter()
2020 .collect()
2021}
2022
2023fn role_names(upgrades: &[ExternalLifecycleRoleUpgradeV1]) -> Vec<String> {
2024 upgrades
2025 .iter()
2026 .filter_map(|upgrade| upgrade.role.clone())
2027 .collect::<BTreeSet<_>>()
2028 .into_iter()
2029 .collect()
2030}
2031
2032fn critical_fix_next_steps(
2033 pending_external_count: usize,
2034 blocked_count: usize,
2035 protected_call_implications: &[String],
2036) -> Vec<String> {
2037 let mut steps = Vec::new();
2038 if pending_external_count > 0 {
2039 steps.push(
2040 "request external consent or completion for externally controlled roles".to_string(),
2041 );
2042 }
2043 if blocked_count > 0 {
2044 steps.push(
2045 "resolve blocked lifecycle rows before reporting the deployment fully patched"
2046 .to_string(),
2047 );
2048 }
2049 if !protected_call_implications.is_empty() {
2050 steps.push(
2051 "review protected-call readiness and role epoch implications before closure"
2052 .to_string(),
2053 );
2054 }
2055 if steps.is_empty() {
2056 steps.push("no external lifecycle work remains for this critical fix".to_string());
2057 }
2058 steps
2059}
2060
2061#[must_use]
2064pub fn critical_external_fix_report_from_pending(
2065 report_id: impl Into<String>,
2066 fix_id: impl Into<String>,
2067 severity: impl Into<String>,
2068 lifecycle_plan: &ExternalLifecyclePlanV1,
2069 pending_report: &ExternalLifecyclePendingReportV1,
2070) -> CriticalExternalFixReportV1 {
2071 let report_id = report_id.into();
2072 let fix_id = fix_id.into();
2073 let severity = severity.into();
2074 let affected_roles = lifecycle_roles(lifecycle_plan);
2075 let affected_canisters = lifecycle_canisters(lifecycle_plan);
2076 let directly_patchable_roles = role_names(&lifecycle_plan.directly_executable_role_upgrades);
2077 let externally_blocked_roles = pending_report
2078 .pending_external_actions
2079 .iter()
2080 .filter_map(|action| action.role.clone())
2081 .collect::<BTreeSet<_>>()
2082 .into_iter()
2083 .collect::<Vec<_>>();
2084 let dependency_blocked_roles = role_names(&lifecycle_plan.blocked_role_upgrades);
2085 let required_external_actions = pending_report
2086 .pending_external_actions
2087 .iter()
2088 .map(|action| format!("{}: {}", action.subject, action.required_external_action))
2089 .collect::<Vec<_>>();
2090 let operator_next_steps = critical_fix_next_steps(
2091 pending_report.pending_external_count,
2092 pending_report.blocked_count,
2093 lifecycle_plan.protected_call_implications.as_slice(),
2094 );
2095 let mut report = CriticalExternalFixReportV1 {
2096 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
2097 report_id,
2098 report_digest: String::new(),
2099 fix_id,
2100 severity,
2101 lifecycle_plan_id: lifecycle_plan.lifecycle_plan_id.clone(),
2102 lifecycle_plan_digest: lifecycle_plan.lifecycle_plan_digest.clone(),
2103 pending_report_id: pending_report.report_id.clone(),
2104 pending_report_digest: pending_report.report_digest.clone(),
2105 deployment_plan_id: lifecycle_plan.deployment_plan_id.clone(),
2106 deployment_plan_digest: lifecycle_plan.deployment_plan_digest.clone(),
2107 inventory_id: lifecycle_plan.inventory_id.clone(),
2108 affected_roles,
2109 affected_canisters,
2110 directly_patchable_roles,
2111 externally_blocked_roles,
2112 dependency_blocked_roles,
2113 required_external_actions,
2114 protected_call_implications: lifecycle_plan.protected_call_implications.clone(),
2115 residual_exposure: pending_report.residual_exposure.clone(),
2116 operator_next_steps,
2117 };
2118 report.report_digest = critical_external_fix_report_digest(&report);
2119 report
2120}
2121
2122pub fn validate_critical_external_fix_report(
2124 report: &CriticalExternalFixReportV1,
2125) -> Result<(), CriticalExternalFixReportError> {
2126 if report.schema_version != DEPLOYMENT_TRUTH_SCHEMA_VERSION {
2127 return Err(CriticalExternalFixReportError::SchemaVersionMismatch {
2128 expected: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
2129 actual: report.schema_version,
2130 });
2131 }
2132 ensure_critical_fix_report_field("report_id", report.report_id.as_str())?;
2133 ensure_critical_fix_report_field("report_digest", report.report_digest.as_str())?;
2134 ensure_critical_fix_report_field("fix_id", report.fix_id.as_str())?;
2135 ensure_critical_fix_report_field("severity", report.severity.as_str())?;
2136 ensure_critical_fix_report_field("lifecycle_plan_id", report.lifecycle_plan_id.as_str())?;
2137 ensure_critical_fix_report_field(
2138 "lifecycle_plan_digest",
2139 report.lifecycle_plan_digest.as_str(),
2140 )?;
2141 ensure_critical_fix_report_field("pending_report_id", report.pending_report_id.as_str())?;
2142 ensure_critical_fix_report_field(
2143 "pending_report_digest",
2144 report.pending_report_digest.as_str(),
2145 )?;
2146 ensure_critical_fix_report_field("deployment_plan_id", report.deployment_plan_id.as_str())?;
2147 ensure_critical_fix_report_field(
2148 "deployment_plan_digest",
2149 report.deployment_plan_digest.as_str(),
2150 )?;
2151 ensure_critical_fix_report_field("inventory_id", report.inventory_id.as_str())?;
2152 if report.report_digest != critical_external_fix_report_digest(report) {
2153 return Err(CriticalExternalFixReportError::DigestMismatch {
2154 field: "report_digest",
2155 });
2156 }
2157 Ok(())
2158}
2159
2160pub fn validate_critical_external_fix_report_for_pending(
2163 report: &CriticalExternalFixReportV1,
2164 lifecycle_plan: &ExternalLifecyclePlanV1,
2165 pending_report: &ExternalLifecyclePendingReportV1,
2166) -> Result<(), CriticalExternalFixReportError> {
2167 validate_critical_external_fix_report(report)?;
2168 if report.lifecycle_plan_id != lifecycle_plan.lifecycle_plan_id {
2169 return Err(CriticalExternalFixReportError::SourceMismatch {
2170 field: "lifecycle_plan_id",
2171 });
2172 }
2173 if report.lifecycle_plan_digest != lifecycle_plan.lifecycle_plan_digest {
2174 return Err(CriticalExternalFixReportError::SourceMismatch {
2175 field: "lifecycle_plan_digest",
2176 });
2177 }
2178 if report.pending_report_id != pending_report.report_id {
2179 return Err(CriticalExternalFixReportError::SourceMismatch {
2180 field: "pending_report_id",
2181 });
2182 }
2183 if report.pending_report_digest != pending_report.report_digest {
2184 return Err(CriticalExternalFixReportError::SourceMismatch {
2185 field: "pending_report_digest",
2186 });
2187 }
2188 let expected = critical_external_fix_report_from_pending(
2189 report.report_id.clone(),
2190 report.fix_id.clone(),
2191 report.severity.clone(),
2192 lifecycle_plan,
2193 pending_report,
2194 );
2195 if report != &expected {
2196 return Err(CriticalExternalFixReportError::SourceMismatch {
2197 field: "lifecycle_plan",
2198 });
2199 }
2200 Ok(())
2201}
2202
2203fn external_upgrade_proposal(
2204 report_id: &str,
2205 lifecycle_plan: &ExternalLifecyclePlanV1,
2206 check: &DeploymentCheckV1,
2207 authority: &LifecycleAuthorityV1,
2208 observed: Option<&ObservedCanisterV1>,
2209 target_artifact: Option<&RoleArtifactV1>,
2210) -> ExternalUpgradeProposalV1 {
2211 let current_module_hash = observed.and_then(|observed| observed.module_hash.clone());
2212 let current_canonical_embedded_config_sha256 =
2213 observed.and_then(|observed| observed.canonical_embedded_config_digest.clone());
2214 let mut proposal = ExternalUpgradeProposalV1 {
2215 proposal_id: external_upgrade_proposal_id(report_id, authority.subject.as_str()),
2216 proposal_digest: String::new(),
2217 deployment_plan_id: lifecycle_plan.deployment_plan_id.clone(),
2218 deployment_plan_digest: lifecycle_plan.deployment_plan_digest.clone(),
2219 lifecycle_plan_id: lifecycle_plan.lifecycle_plan_id.clone(),
2220 lifecycle_plan_digest: lifecycle_plan.lifecycle_plan_digest.clone(),
2221 promotion_plan_id: None,
2222 promotion_plan_digest: None,
2223 promotion_provenance_id: None,
2224 promotion_provenance_digest: None,
2225 subject: authority.subject.clone(),
2226 canister_id: authority.canister_id.clone(),
2227 role: authority.role.clone(),
2228 control_class: authority.control_class,
2229 lifecycle_mode: authority.lifecycle_mode,
2230 observed_before_digest: observed_before_digest(
2231 authority,
2232 current_module_hash.as_ref(),
2233 current_canonical_embedded_config_sha256.as_ref(),
2234 ),
2235 current_module_hash,
2236 current_canonical_embedded_config_sha256,
2237 target_wasm_sha256: target_artifact.and_then(|artifact| artifact.wasm_sha256.clone()),
2238 target_wasm_gz_sha256: target_artifact.and_then(|artifact| artifact.wasm_gz_sha256.clone()),
2239 target_installed_module_hash: target_artifact
2240 .and_then(|artifact| artifact.installed_module_hash.clone()),
2241 target_role_artifact_identity: target_artifact.map(role_artifact_identity),
2242 target_canonical_embedded_config_sha256: target_artifact
2243 .and_then(|artifact| artifact.canonical_embedded_config_sha256.clone()),
2244 root_trust_anchor: check.plan.trust_domain.root_trust_anchor.clone(),
2245 authority_profile_hash: check
2246 .plan
2247 .deployment_identity
2248 .authority_profile_hash
2249 .clone(),
2250 required_external_action: required_external_action(authority.lifecycle_mode).to_string(),
2251 consent_requirements: authority.consent_requirements.clone(),
2252 allowed_authorization_modes: external_upgrade_authorization_modes(authority.control_class),
2253 verification_requirements: authority.verification_requirements.clone(),
2254 expires_at: None,
2255 supersedes_proposal_id: None,
2256 };
2257 proposal.proposal_digest = external_upgrade_proposal_digest(&proposal);
2258 proposal
2259}
2260
2261fn validate_external_upgrade_proposal(
2262 proposal: &ExternalUpgradeProposalV1,
2263) -> Result<(), ExternalUpgradeProposalReportError> {
2264 ensure_external_proposal_report_field("proposal_id", proposal.proposal_id.as_str())?;
2265 ensure_external_proposal_report_field("proposal_digest", proposal.proposal_digest.as_str())?;
2266 ensure_external_proposal_report_field(
2267 "proposal.deployment_plan_id",
2268 proposal.deployment_plan_id.as_str(),
2269 )?;
2270 ensure_external_proposal_report_field(
2271 "proposal.deployment_plan_digest",
2272 proposal.deployment_plan_digest.as_str(),
2273 )?;
2274 ensure_external_proposal_report_field(
2275 "proposal.lifecycle_plan_id",
2276 proposal.lifecycle_plan_id.as_str(),
2277 )?;
2278 ensure_external_proposal_report_field(
2279 "proposal.lifecycle_plan_digest",
2280 proposal.lifecycle_plan_digest.as_str(),
2281 )?;
2282 ensure_external_proposal_report_field(
2283 "proposal.observed_before_digest",
2284 proposal.observed_before_digest.as_str(),
2285 )?;
2286 ensure_external_proposal_report_field("proposal.subject", proposal.subject.as_str())?;
2287 if proposal.lifecycle_mode == LifecycleModeV1::DirectDeploymentAuthority {
2288 return Err(
2289 ExternalUpgradeProposalReportError::DirectLifecycleProposal {
2290 subject: proposal.subject.clone(),
2291 },
2292 );
2293 }
2294 if proposal.proposal_digest != external_upgrade_proposal_digest(proposal) {
2295 return Err(ExternalUpgradeProposalReportError::DigestMismatch {
2296 field: "proposal_digest",
2297 });
2298 }
2299 Ok(())
2300}
2301
2302fn lifecycle_authority_for_expected_canister(
2303 plan: &DeploymentPlanV1,
2304 expected: &ExpectedCanisterV1,
2305 observed: Option<&ObservedCanisterV1>,
2306) -> LifecycleAuthorityV1 {
2307 let canister_id = expected
2308 .canister_id
2309 .clone()
2310 .or_else(|| observed.map(|observed| observed.canister_id.clone()));
2311 let role = Some(expected.role.clone());
2312 let control_class = observed.map_or(expected.control_class, |observed| observed.control_class);
2313 let observed_controllers =
2314 observed.map_or_else(Vec::new, |observed| observed.controllers.clone());
2315 lifecycle_authority(
2316 lifecycle_subject_for_parts(canister_id.as_deref(), role.as_deref()),
2317 canister_id,
2318 role,
2319 control_class,
2320 observed_controllers,
2321 &plan.authority_profile.expected_controllers,
2322 plan.expected_verifier_readiness.required,
2323 )
2324}
2325
2326fn lifecycle_authority_for_expected_pool(
2327 expected: &ExpectedPoolCanisterV1,
2328 observed: Option<&ObservedPoolCanisterV1>,
2329) -> LifecycleAuthorityV1 {
2330 let canister_id = expected
2331 .canister_id
2332 .clone()
2333 .or_else(|| observed.map(|observed| observed.canister_id.clone()));
2334 let role = expected
2335 .role
2336 .clone()
2337 .or_else(|| observed.and_then(|observed| observed.role.clone()));
2338 let control_class = observed.map_or(CanisterControlClassV1::CanicManagedPool, |observed| {
2339 observed.control_class
2340 });
2341 lifecycle_authority(
2342 lifecycle_subject_for_parts(canister_id.as_deref(), role.as_deref()),
2343 canister_id,
2344 role,
2345 control_class,
2346 Vec::new(),
2347 &[],
2348 false,
2349 )
2350}
2351
2352fn lifecycle_authority_for_unplanned_canister(
2353 observed: &ObservedCanisterV1,
2354) -> LifecycleAuthorityV1 {
2355 lifecycle_authority(
2356 lifecycle_subject(observed.canister_id.as_str(), observed.role.as_deref()),
2357 Some(observed.canister_id.clone()),
2358 observed.role.clone(),
2359 observed.control_class,
2360 observed.controllers.clone(),
2361 &[],
2362 false,
2363 )
2364}
2365
2366fn lifecycle_authority_for_unplanned_pool(
2367 observed: &ObservedPoolCanisterV1,
2368) -> LifecycleAuthorityV1 {
2369 lifecycle_authority(
2370 lifecycle_subject(observed.canister_id.as_str(), observed.role.as_deref()),
2371 Some(observed.canister_id.clone()),
2372 observed.role.clone(),
2373 observed.control_class,
2374 Vec::new(),
2375 &[],
2376 false,
2377 )
2378}
2379
2380fn lifecycle_authority(
2381 subject: String,
2382 canister_id: Option<String>,
2383 role: Option<String>,
2384 control_class: CanisterControlClassV1,
2385 observed_controllers: Vec<String>,
2386 expected_controllers: &[String],
2387 verifier_required: bool,
2388) -> LifecycleAuthorityV1 {
2389 let required_controllers = required_lifecycle_controllers(control_class, expected_controllers);
2390 let external_controllers =
2391 external_lifecycle_controllers(control_class, &observed_controllers, &required_controllers);
2392 let consent_requirements = lifecycle_consent_requirements(control_class, &external_controllers);
2393 let allowed_upgrade_modes = lifecycle_upgrade_modes(control_class);
2394 let verification_requirements = lifecycle_verification_requirements(verifier_required);
2395 let external_action_required = lifecycle_external_action_required(control_class);
2396 let blocked = control_class == CanisterControlClassV1::UnknownUnsafe;
2397 let lifecycle_mode = lifecycle_mode(control_class);
2398 let blockers = lifecycle_blockers(control_class);
2399 let warnings = lifecycle_warnings(control_class);
2400 let reason = lifecycle_reason(control_class);
2401 LifecycleAuthorityV1 {
2402 subject,
2403 canister_id,
2404 role,
2405 control_class,
2406 lifecycle_mode,
2407 observed_controllers,
2408 expected_deployment_controllers: sorted_unique(expected_controllers.to_vec()),
2409 external_controllers,
2410 required_controllers,
2411 consent_requirements,
2412 allowed_upgrade_modes,
2413 verification_requirements,
2414 external_action_required,
2415 blocked,
2416 blockers,
2417 warnings,
2418 reason,
2419 }
2420}
2421
2422fn required_lifecycle_controllers(
2423 control_class: CanisterControlClassV1,
2424 expected_controllers: &[String],
2425) -> Vec<String> {
2426 match control_class {
2427 CanisterControlClassV1::DeploymentControlled
2428 | CanisterControlClassV1::JointlyControlled => sorted_unique(expected_controllers.to_vec()),
2429 CanisterControlClassV1::CanicManagedPool
2430 | CanisterControlClassV1::ExternallyImported
2431 | CanisterControlClassV1::UserControlled
2432 | CanisterControlClassV1::UnknownUnsafe => Vec::new(),
2433 }
2434}
2435
2436fn external_lifecycle_controllers(
2437 control_class: CanisterControlClassV1,
2438 observed_controllers: &[String],
2439 required_controllers: &[String],
2440) -> Vec<String> {
2441 match control_class {
2442 CanisterControlClassV1::DeploymentControlled | CanisterControlClassV1::UnknownUnsafe => {
2443 Vec::new()
2444 }
2445 CanisterControlClassV1::JointlyControlled => {
2446 let required = required_controllers.iter().collect::<BTreeSet<_>>();
2447 sorted_unique(
2448 observed_controllers
2449 .iter()
2450 .filter(|controller| !required.contains(controller))
2451 .cloned()
2452 .collect(),
2453 )
2454 }
2455 CanisterControlClassV1::CanicManagedPool
2456 | CanisterControlClassV1::ExternallyImported
2457 | CanisterControlClassV1::UserControlled => sorted_unique(observed_controllers.to_vec()),
2458 }
2459}
2460
2461fn lifecycle_consent_requirements(
2462 control_class: CanisterControlClassV1,
2463 external_controllers: &[String],
2464) -> Vec<ConsentRequirementV1> {
2465 if !lifecycle_external_action_required(control_class) {
2466 return Vec::new();
2467 }
2468 vec![ConsentRequirementV1 {
2469 consent_subject_kind: consent_subject_kind(control_class),
2470 required_principals: sorted_unique(external_controllers.to_vec()),
2471 required_controller_set_digest: Some(stable_json_sha256_hex(&external_controllers)),
2472 consent_channel_kind: consent_channel_kind(control_class),
2473 required_action: required_consent_action(control_class),
2474 }]
2475}
2476
2477const fn consent_subject_kind(control_class: CanisterControlClassV1) -> ConsentSubjectKindV1 {
2478 match control_class {
2479 CanisterControlClassV1::CanicManagedPool => ConsentSubjectKindV1::ProjectHub,
2480 CanisterControlClassV1::ExternallyImported | CanisterControlClassV1::JointlyControlled => {
2481 ConsentSubjectKindV1::CustomerController
2482 }
2483 CanisterControlClassV1::UserControlled => ConsentSubjectKindV1::UserPrincipal,
2484 CanisterControlClassV1::DeploymentControlled | CanisterControlClassV1::UnknownUnsafe => {
2485 ConsentSubjectKindV1::UnknownExternalController
2486 }
2487 }
2488}
2489
2490const fn consent_channel_kind(control_class: CanisterControlClassV1) -> ConsentChannelKindV1 {
2491 match control_class {
2492 CanisterControlClassV1::CanicManagedPool => ConsentChannelKindV1::DelegatedInstall,
2493 CanisterControlClassV1::ExternallyImported
2494 | CanisterControlClassV1::JointlyControlled
2495 | CanisterControlClassV1::UserControlled => ConsentChannelKindV1::GeneratedCommand,
2496 CanisterControlClassV1::DeploymentControlled | CanisterControlClassV1::UnknownUnsafe => {
2497 ConsentChannelKindV1::OutOfBand
2498 }
2499 }
2500}
2501
2502const fn required_consent_action(
2503 control_class: CanisterControlClassV1,
2504) -> ExternalUpgradeAuthorizationModeV1 {
2505 match control_class {
2506 CanisterControlClassV1::JointlyControlled => {
2507 ExternalUpgradeAuthorizationModeV1::ConsentForDirectInstall
2508 }
2509 CanisterControlClassV1::CanicManagedPool => {
2510 ExternalUpgradeAuthorizationModeV1::DelegatedInstallAuthority
2511 }
2512 CanisterControlClassV1::ExternallyImported | CanisterControlClassV1::UserControlled => {
2513 ExternalUpgradeAuthorizationModeV1::ExternalControllerExecution
2514 }
2515 CanisterControlClassV1::DeploymentControlled | CanisterControlClassV1::UnknownUnsafe => {
2516 ExternalUpgradeAuthorizationModeV1::ObserveAndVerifyOnly
2517 }
2518 }
2519}
2520
2521const fn lifecycle_mode(control_class: CanisterControlClassV1) -> LifecycleModeV1 {
2522 match control_class {
2523 CanisterControlClassV1::DeploymentControlled => LifecycleModeV1::DirectDeploymentAuthority,
2524 CanisterControlClassV1::CanicManagedPool => LifecycleModeV1::DelegatedInstallRequired,
2525 CanisterControlClassV1::ExternallyImported | CanisterControlClassV1::UserControlled => {
2526 LifecycleModeV1::ExternalCompletionOnly
2527 }
2528 CanisterControlClassV1::JointlyControlled => LifecycleModeV1::ProposalRequired,
2529 CanisterControlClassV1::UnknownUnsafe => LifecycleModeV1::UnknownUnsafeBlocked,
2530 }
2531}
2532
2533fn lifecycle_blockers(control_class: CanisterControlClassV1) -> Vec<String> {
2534 if control_class == CanisterControlClassV1::UnknownUnsafe {
2535 vec!["unknown unsafe controller state blocks lifecycle action".to_string()]
2536 } else {
2537 Vec::new()
2538 }
2539}
2540
2541fn lifecycle_warnings(control_class: CanisterControlClassV1) -> Vec<String> {
2542 match control_class {
2543 CanisterControlClassV1::CanicManagedPool => {
2544 vec!["pool-aware lifecycle policy is required before mutation".to_string()]
2545 }
2546 CanisterControlClassV1::ExternallyImported => {
2547 vec!["external controller action or verification is required".to_string()]
2548 }
2549 CanisterControlClassV1::JointlyControlled => {
2550 vec!["joint controller consent or delegation is required".to_string()]
2551 }
2552 CanisterControlClassV1::UserControlled => {
2553 vec!["user or delegated lifecycle action is required".to_string()]
2554 }
2555 CanisterControlClassV1::DeploymentControlled | CanisterControlClassV1::UnknownUnsafe => {
2556 Vec::new()
2557 }
2558 }
2559}
2560
2561fn lifecycle_upgrade_modes(control_class: CanisterControlClassV1) -> Vec<LifecycleUpgradeModeV1> {
2562 match control_class {
2563 CanisterControlClassV1::DeploymentControlled => vec![
2564 LifecycleUpgradeModeV1::DirectByDeploymentAuthority,
2565 LifecycleUpgradeModeV1::VerifyExternalCompletion,
2566 ],
2567 CanisterControlClassV1::CanicManagedPool
2568 | CanisterControlClassV1::ExternallyImported
2569 | CanisterControlClassV1::JointlyControlled
2570 | CanisterControlClassV1::UserControlled => vec![
2571 LifecycleUpgradeModeV1::ExternalProposal,
2572 LifecycleUpgradeModeV1::ExternalExecution,
2573 LifecycleUpgradeModeV1::VerifyExternalCompletion,
2574 LifecycleUpgradeModeV1::ObserveOnly,
2575 ],
2576 CanisterControlClassV1::UnknownUnsafe => vec![LifecycleUpgradeModeV1::Blocked],
2577 }
2578}
2579
2580fn lifecycle_verification_requirements(
2581 verifier_required: bool,
2582) -> Vec<LifecycleVerificationRequirementV1> {
2583 let mut requirements = vec![
2584 LifecycleVerificationRequirementV1::LiveInventory,
2585 LifecycleVerificationRequirementV1::ControllerObservation,
2586 LifecycleVerificationRequirementV1::ModuleHash,
2587 LifecycleVerificationRequirementV1::CanonicalEmbeddedConfig,
2588 ];
2589 if verifier_required {
2590 requirements.push(LifecycleVerificationRequirementV1::ProtectedCallReadiness);
2591 }
2592 requirements
2593}
2594
2595const fn lifecycle_external_action_required(control_class: CanisterControlClassV1) -> bool {
2596 matches!(
2597 control_class,
2598 CanisterControlClassV1::CanicManagedPool
2599 | CanisterControlClassV1::ExternallyImported
2600 | CanisterControlClassV1::JointlyControlled
2601 | CanisterControlClassV1::UserControlled
2602 )
2603}
2604
2605fn lifecycle_reason(control_class: CanisterControlClassV1) -> String {
2606 match control_class {
2607 CanisterControlClassV1::DeploymentControlled => {
2608 "deployment authority can execute lifecycle directly".to_string()
2609 }
2610 CanisterControlClassV1::CanicManagedPool => {
2611 "Canic-managed pool lifecycle requires pool-aware external action".to_string()
2612 }
2613 CanisterControlClassV1::ExternallyImported => {
2614 "externally imported canister requires external controller action".to_string()
2615 }
2616 CanisterControlClassV1::JointlyControlled => {
2617 "jointly controlled canister requires non-deployment-controller consent".to_string()
2618 }
2619 CanisterControlClassV1::UserControlled => {
2620 "user-controlled canister requires user or delegated lifecycle action".to_string()
2621 }
2622 CanisterControlClassV1::UnknownUnsafe => {
2623 "unknown or unsafe controller state blocks lifecycle action".to_string()
2624 }
2625 }
2626}
2627
2628fn observed_canister_for_expected<'a>(
2629 inventory: &'a DeploymentInventoryV1,
2630 expected: &ExpectedCanisterV1,
2631) -> Option<&'a ObservedCanisterV1> {
2632 if let Some(canister_id) = &expected.canister_id
2633 && let Some(observed) = inventory
2634 .observed_canisters
2635 .iter()
2636 .find(|observed| &observed.canister_id == canister_id)
2637 {
2638 return Some(observed);
2639 }
2640 inventory
2641 .observed_canisters
2642 .iter()
2643 .find(|observed| observed.role.as_deref() == Some(expected.role.as_str()))
2644}
2645
2646fn observed_pool_for_expected<'a>(
2647 inventory: &'a DeploymentInventoryV1,
2648 expected: &ExpectedPoolCanisterV1,
2649) -> Option<&'a ObservedPoolCanisterV1> {
2650 if let Some(canister_id) = &expected.canister_id
2651 && let Some(observed) = inventory
2652 .observed_pool
2653 .iter()
2654 .find(|observed| &observed.canister_id == canister_id)
2655 {
2656 return Some(observed);
2657 }
2658 inventory.observed_pool.iter().find(|observed| {
2659 observed.pool == expected.pool && observed.role.as_deref() == expected.role.as_deref()
2660 })
2661}
2662
2663fn lifecycle_subject(canister_id: &str, role: Option<&str>) -> String {
2664 lifecycle_subject_for_parts(Some(canister_id), role)
2665}
2666
2667fn lifecycle_subject_for_parts(canister_id: Option<&str>, role: Option<&str>) -> String {
2668 match (role, canister_id) {
2669 (Some(role), Some(canister_id)) => format!("{role}:{canister_id}"),
2670 (Some(role), None) => format!("{role}:unassigned"),
2671 (None, Some(canister_id)) => canister_id.to_string(),
2672 (None, None) => "unknown".to_string(),
2673 }
2674}
2675
2676fn observed_canister_for_authority<'a>(
2677 inventory: &'a DeploymentInventoryV1,
2678 authority: &LifecycleAuthorityV1,
2679) -> Option<&'a ObservedCanisterV1> {
2680 if let Some(canister_id) = &authority.canister_id
2681 && let Some(observed) = inventory
2682 .observed_canisters
2683 .iter()
2684 .find(|observed| &observed.canister_id == canister_id)
2685 {
2686 return Some(observed);
2687 }
2688 inventory
2689 .observed_canisters
2690 .iter()
2691 .find(|observed| observed.role == authority.role)
2692}
2693
2694fn target_artifact_for_authority<'a>(
2695 plan: &'a DeploymentPlanV1,
2696 authority: &LifecycleAuthorityV1,
2697) -> Option<&'a RoleArtifactV1> {
2698 let role = authority.role.as_ref()?;
2699 plan.role_artifacts
2700 .iter()
2701 .find(|artifact| &artifact.role == role)
2702}
2703
2704fn external_lifecycle_role_upgrade(
2705 authority: &LifecycleAuthorityV1,
2706) -> ExternalLifecycleRoleUpgradeV1 {
2707 ExternalLifecycleRoleUpgradeV1 {
2708 subject: authority.subject.clone(),
2709 canister_id: authority.canister_id.clone(),
2710 role: authority.role.clone(),
2711 control_class: authority.control_class,
2712 lifecycle_mode: authority.lifecycle_mode,
2713 required_external_action: authority
2714 .external_action_required
2715 .then(|| required_external_action(authority.lifecycle_mode).to_string()),
2716 blockers: authority.blockers.clone(),
2717 warnings: authority.warnings.clone(),
2718 }
2719}
2720
2721fn protected_call_implications_for_check(check: &DeploymentCheckV1) -> Vec<String> {
2722 if check.plan.expected_verifier_readiness.required {
2723 vec!["protected-call verifier readiness must be checked before completion".to_string()]
2724 } else {
2725 Vec::new()
2726 }
2727}
2728
2729const fn required_external_action(lifecycle_mode: LifecycleModeV1) -> &'static str {
2730 match lifecycle_mode {
2731 LifecycleModeV1::DirectDeploymentAuthority => "none",
2732 LifecycleModeV1::ProposalRequired => "proposal_and_consent",
2733 LifecycleModeV1::DelegatedInstallRequired => "delegated_install_or_pool_policy",
2734 LifecycleModeV1::ExternalCompletionOnly => "external_controller_execution",
2735 LifecycleModeV1::VerifyOnly => "verify_external_completion",
2736 LifecycleModeV1::MustNotTouch | LifecycleModeV1::UnknownUnsafeBlocked => "blocked",
2737 }
2738}
2739
2740fn role_artifact_identity(artifact: &RoleArtifactV1) -> String {
2741 stable_json_sha256_hex(&(
2742 artifact.role.as_str(),
2743 artifact.wasm_sha256.as_deref(),
2744 artifact.wasm_gz_sha256.as_deref(),
2745 artifact.installed_module_hash.as_deref(),
2746 artifact.candid_sha256.as_deref(),
2747 artifact.canonical_embedded_config_sha256.as_deref(),
2748 ))
2749}
2750
2751fn external_upgrade_authorization_modes(
2752 control_class: CanisterControlClassV1,
2753) -> Vec<ExternalUpgradeAuthorizationModeV1> {
2754 match control_class {
2755 CanisterControlClassV1::JointlyControlled => vec![
2756 ExternalUpgradeAuthorizationModeV1::ConsentForDirectInstall,
2757 ExternalUpgradeAuthorizationModeV1::DelegatedInstallAuthority,
2758 ExternalUpgradeAuthorizationModeV1::ExternalControllerExecution,
2759 ExternalUpgradeAuthorizationModeV1::ObserveAndVerifyOnly,
2760 ],
2761 CanisterControlClassV1::CanicManagedPool
2762 | CanisterControlClassV1::ExternallyImported
2763 | CanisterControlClassV1::UserControlled => vec![
2764 ExternalUpgradeAuthorizationModeV1::DelegatedInstallAuthority,
2765 ExternalUpgradeAuthorizationModeV1::ExternalControllerExecution,
2766 ExternalUpgradeAuthorizationModeV1::ObserveAndVerifyOnly,
2767 ],
2768 CanisterControlClassV1::DeploymentControlled | CanisterControlClassV1::UnknownUnsafe => {
2769 Vec::new()
2770 }
2771 }
2772}
2773
2774fn external_upgrade_proposal_id(report_id: &str, subject: &str) -> String {
2775 let subject = subject.replace([':', '/'], "-");
2776 format!("{report_id}:{subject}")
2777}
2778
2779fn external_lifecycle_plan_digest(plan: &ExternalLifecyclePlanV1) -> String {
2780 stable_json_sha256_hex(&ExternalLifecyclePlanDigestInput {
2781 lifecycle_authority_report_id: &plan.lifecycle_authority_report_id,
2782 deployment_plan_id: &plan.deployment_plan_id,
2783 deployment_plan_digest: &plan.deployment_plan_digest,
2784 inventory_id: &plan.inventory_id,
2785 lifecycle_authority_rows: &plan.lifecycle_authority_rows,
2786 directly_executable_role_upgrades: &plan.directly_executable_role_upgrades,
2787 proposed_external_role_upgrades: &plan.proposed_external_role_upgrades,
2788 blocked_role_upgrades: &plan.blocked_role_upgrades,
2789 dependency_blockers: &plan.dependency_blockers,
2790 protected_call_implications: &plan.protected_call_implications,
2791 residual_exposure: &plan.residual_exposure,
2792 status: plan.status,
2793 })
2794}
2795
2796fn lifecycle_authority_report_digest(report: &LifecycleAuthorityReportV1) -> String {
2797 stable_json_sha256_hex(&LifecycleAuthorityReportDigestInput {
2798 report_id: &report.report_id,
2799 check_id: &report.check_id,
2800 plan_id: &report.plan_id,
2801 inventory_id: &report.inventory_id,
2802 authorities: &report.authorities,
2803 external_action_required_count: report.external_action_required_count,
2804 blocked_count: report.blocked_count,
2805 })
2806}
2807
2808const fn expected_lifecycle_plan_status(
2809 plan: &ExternalLifecyclePlanV1,
2810) -> ExternalLifecyclePlanStatusV1 {
2811 if !plan.blocked_role_upgrades.is_empty() {
2812 ExternalLifecyclePlanStatusV1::Blocked
2813 } else if !plan.proposed_external_role_upgrades.is_empty() {
2814 ExternalLifecyclePlanStatusV1::PendingExternalAction
2815 } else {
2816 ExternalLifecyclePlanStatusV1::Ready
2817 }
2818}
2819
2820fn ensure_unique_lifecycle_subjects(
2821 rows: &[LifecycleAuthorityV1],
2822) -> Result<(), ExternalLifecyclePlanError> {
2823 let mut subjects = BTreeSet::new();
2824 for row in rows {
2825 if !subjects.insert(row.subject.clone()) {
2826 return Err(ExternalLifecyclePlanError::DuplicateSubject {
2827 subject: row.subject.clone(),
2828 });
2829 }
2830 }
2831 Ok(())
2832}
2833
2834fn ensure_unique_authority_subjects(
2835 rows: &[LifecycleAuthorityV1],
2836) -> Result<(), LifecycleAuthorityReportError> {
2837 let mut subjects = BTreeSet::new();
2838 for row in rows {
2839 if !subjects.insert(row.subject.clone()) {
2840 return Err(LifecycleAuthorityReportError::DuplicateSubject {
2841 subject: row.subject.clone(),
2842 });
2843 }
2844 }
2845 Ok(())
2846}
2847
2848fn ensure_unique_role_upgrade_subjects(
2849 rows: &[ExternalLifecycleRoleUpgradeV1],
2850) -> Result<(), ExternalLifecyclePlanError> {
2851 let mut subjects = BTreeSet::new();
2852 for row in rows {
2853 if !subjects.insert(row.subject.clone()) {
2854 return Err(ExternalLifecyclePlanError::DuplicateSubject {
2855 subject: row.subject.clone(),
2856 });
2857 }
2858 }
2859 Ok(())
2860}
2861
2862fn external_upgrade_proposal_digest(proposal: &ExternalUpgradeProposalV1) -> String {
2863 stable_json_sha256_hex(&ExternalUpgradeProposalDigestInput {
2864 deployment_plan_id: &proposal.deployment_plan_id,
2865 deployment_plan_digest: &proposal.deployment_plan_digest,
2866 lifecycle_plan_id: &proposal.lifecycle_plan_id,
2867 lifecycle_plan_digest: &proposal.lifecycle_plan_digest,
2868 promotion_plan_id: &proposal.promotion_plan_id,
2869 promotion_plan_digest: &proposal.promotion_plan_digest,
2870 promotion_provenance_id: &proposal.promotion_provenance_id,
2871 promotion_provenance_digest: &proposal.promotion_provenance_digest,
2872 subject: &proposal.subject,
2873 canister_id: &proposal.canister_id,
2874 role: &proposal.role,
2875 control_class: proposal.control_class,
2876 lifecycle_mode: proposal.lifecycle_mode,
2877 observed_before_digest: &proposal.observed_before_digest,
2878 current_module_hash: &proposal.current_module_hash,
2879 current_canonical_embedded_config_sha256: &proposal
2880 .current_canonical_embedded_config_sha256,
2881 target_wasm_sha256: &proposal.target_wasm_sha256,
2882 target_wasm_gz_sha256: &proposal.target_wasm_gz_sha256,
2883 target_installed_module_hash: &proposal.target_installed_module_hash,
2884 target_role_artifact_identity: &proposal.target_role_artifact_identity,
2885 target_canonical_embedded_config_sha256: &proposal.target_canonical_embedded_config_sha256,
2886 root_trust_anchor: &proposal.root_trust_anchor,
2887 authority_profile_hash: &proposal.authority_profile_hash,
2888 required_external_action: &proposal.required_external_action,
2889 consent_requirements: &proposal.consent_requirements,
2890 allowed_authorization_modes: &proposal.allowed_authorization_modes,
2891 verification_requirements: &proposal.verification_requirements,
2892 expires_at: &proposal.expires_at,
2893 supersedes_proposal_id: &proposal.supersedes_proposal_id,
2894 })
2895}
2896
2897fn external_upgrade_proposal_report_digest(report: &ExternalUpgradeProposalReportV1) -> String {
2898 stable_json_sha256_hex(&ExternalUpgradeProposalReportDigestInput {
2899 report_id: &report.report_id,
2900 lifecycle_plan_id: &report.lifecycle_plan_id,
2901 lifecycle_plan_digest: &report.lifecycle_plan_digest,
2902 deployment_plan_id: &report.deployment_plan_id,
2903 deployment_plan_digest: &report.deployment_plan_digest,
2904 inventory_id: &report.inventory_id,
2905 proposals: &report.proposals,
2906 blocked_subjects: &report.blocked_subjects,
2907 })
2908}
2909
2910fn external_lifecycle_pending_report_digest(report: &ExternalLifecyclePendingReportV1) -> String {
2911 stable_json_sha256_hex(&ExternalLifecyclePendingReportDigestInput {
2912 report_id: &report.report_id,
2913 lifecycle_plan_id: &report.lifecycle_plan_id,
2914 lifecycle_plan_digest: &report.lifecycle_plan_digest,
2915 proposal_report_id: &report.proposal_report_id,
2916 proposal_report_digest: &report.proposal_report_digest,
2917 deployment_plan_id: &report.deployment_plan_id,
2918 deployment_plan_digest: &report.deployment_plan_digest,
2919 inventory_id: &report.inventory_id,
2920 direct_upgrade_count: report.direct_upgrade_count,
2921 pending_external_count: report.pending_external_count,
2922 blocked_count: report.blocked_count,
2923 pending_external_actions: &report.pending_external_actions,
2924 blocked_subjects: &report.blocked_subjects,
2925 residual_exposure: &report.residual_exposure,
2926 status: report.status,
2927 })
2928}
2929
2930fn external_lifecycle_check_digest(check: &ExternalLifecycleCheckV1) -> String {
2931 stable_json_sha256_hex(&ExternalLifecycleCheckDigestInput {
2932 check_id: &check.check_id,
2933 lifecycle_plan_id: &check.lifecycle_plan_id,
2934 lifecycle_plan_digest: &check.lifecycle_plan_digest,
2935 proposal_report_id: &check.proposal_report_id,
2936 proposal_report_digest: &check.proposal_report_digest,
2937 pending_report_id: &check.pending_report_id,
2938 pending_report_digest: &check.pending_report_digest,
2939 deployment_plan_id: &check.deployment_plan_id,
2940 deployment_plan_digest: &check.deployment_plan_digest,
2941 inventory_id: &check.inventory_id,
2942 status: check.status,
2943 direct_upgrade_count: check.direct_upgrade_count,
2944 pending_external_count: check.pending_external_count,
2945 blocked_count: check.blocked_count,
2946 residual_exposure_count: check.residual_exposure_count,
2947 summary: &check.summary,
2948 next_actions: &check.next_actions,
2949 })
2950}
2951
2952fn external_lifecycle_handoff_digest(handoff: &ExternalLifecycleHandoffV1) -> String {
2953 stable_json_sha256_hex(&ExternalLifecycleHandoffDigestInput {
2954 handoff_id: &handoff.handoff_id,
2955 lifecycle_check_id: &handoff.lifecycle_check_id,
2956 lifecycle_check_digest: &handoff.lifecycle_check_digest,
2957 pending_report_id: &handoff.pending_report_id,
2958 pending_report_digest: &handoff.pending_report_digest,
2959 proposal_report_id: &handoff.proposal_report_id,
2960 proposal_report_digest: &handoff.proposal_report_digest,
2961 deployment_plan_id: &handoff.deployment_plan_id,
2962 deployment_plan_digest: &handoff.deployment_plan_digest,
2963 inventory_id: &handoff.inventory_id,
2964 status: handoff.status,
2965 handoff_actions: &handoff.handoff_actions,
2966 blocked_subjects: &handoff.blocked_subjects,
2967 residual_exposure: &handoff.residual_exposure,
2968 operator_summary: &handoff.operator_summary,
2969 })
2970}
2971
2972fn critical_external_fix_report_digest(report: &CriticalExternalFixReportV1) -> String {
2973 stable_json_sha256_hex(&CriticalExternalFixReportDigestInput {
2974 report_id: &report.report_id,
2975 fix_id: &report.fix_id,
2976 severity: &report.severity,
2977 lifecycle_plan_id: &report.lifecycle_plan_id,
2978 lifecycle_plan_digest: &report.lifecycle_plan_digest,
2979 pending_report_id: &report.pending_report_id,
2980 pending_report_digest: &report.pending_report_digest,
2981 deployment_plan_id: &report.deployment_plan_id,
2982 deployment_plan_digest: &report.deployment_plan_digest,
2983 inventory_id: &report.inventory_id,
2984 affected_roles: &report.affected_roles,
2985 affected_canisters: &report.affected_canisters,
2986 directly_patchable_roles: &report.directly_patchable_roles,
2987 externally_blocked_roles: &report.externally_blocked_roles,
2988 dependency_blocked_roles: &report.dependency_blocked_roles,
2989 required_external_actions: &report.required_external_actions,
2990 protected_call_implications: &report.protected_call_implications,
2991 residual_exposure: &report.residual_exposure,
2992 operator_next_steps: &report.operator_next_steps,
2993 })
2994}
2995
2996fn external_upgrade_receipt_digest(receipt: &ExternalUpgradeReceiptV1) -> String {
2997 stable_json_sha256_hex(&ExternalUpgradeReceiptDigestInput {
2998 proposal_id: &receipt.proposal_id,
2999 proposal_digest: &receipt.proposal_digest,
3000 subject: &receipt.subject,
3001 canister_id: &receipt.canister_id,
3002 role: &receipt.role,
3003 consent_state: receipt.consent_state,
3004 reported_by: &receipt.reported_by,
3005 observed_before_module_hash: &receipt.observed_before_module_hash,
3006 observed_after_module_hash: &receipt.observed_after_module_hash,
3007 observed_after_canonical_embedded_config_sha256: &receipt
3008 .observed_after_canonical_embedded_config_sha256,
3009 verification_result: receipt.verification_result,
3010 verification_notes: &receipt.verification_notes,
3011 })
3012}
3013
3014fn external_upgrade_consent_evidence_digest(evidence: &ExternalUpgradeConsentEvidenceV1) -> String {
3015 stable_json_sha256_hex(&ExternalUpgradeConsentEvidenceDigestInput {
3016 evidence_id: &evidence.evidence_id,
3017 proposal_id: &evidence.proposal_id,
3018 proposal_digest: &evidence.proposal_digest,
3019 receipt_id: &evidence.receipt_id,
3020 receipt_digest: &evidence.receipt_digest,
3021 subject: &evidence.subject,
3022 canister_id: &evidence.canister_id,
3023 role: &evidence.role,
3024 consent_state: evidence.consent_state,
3025 reported_by: &evidence.reported_by,
3026 consent_requirements: &evidence.consent_requirements,
3027 allowed_authorization_modes: &evidence.allowed_authorization_modes,
3028 status_summary: &evidence.status_summary,
3029 })
3030}
3031
3032fn external_upgrade_verification_report_digest(
3033 report: &ExternalUpgradeVerificationReportV1,
3034) -> String {
3035 stable_json_sha256_hex(&ExternalUpgradeVerificationReportDigestInput {
3036 report_id: &report.report_id,
3037 proposal_id: &report.proposal_id,
3038 proposal_digest: &report.proposal_digest,
3039 receipt_id: &report.receipt_id,
3040 receipt_digest: &report.receipt_digest,
3041 subject: &report.subject,
3042 canister_id: &report.canister_id,
3043 role: &report.role,
3044 verification_result: report.verification_result,
3045 verification_notes: &report.verification_notes,
3046 live_inventory_required: report.live_inventory_required,
3047 status_summary: &report.status_summary,
3048 })
3049}
3050
3051fn external_upgrade_verification_policy_digest(
3052 policy: &ExternalUpgradeVerificationPolicyV1,
3053) -> String {
3054 stable_json_sha256_hex(&ExternalUpgradeVerificationPolicyDigestInput {
3055 policy_id: &policy.policy_id,
3056 proposal_id: &policy.proposal_id,
3057 proposal_digest: &policy.proposal_digest,
3058 subject: &policy.subject,
3059 canister_id: &policy.canister_id,
3060 role: &policy.role,
3061 required_verification: &policy.required_verification,
3062 verification_requirements: &policy.verification_requirements,
3063 max_observation_age_seconds: policy.max_observation_age_seconds,
3064 status_summary: &policy.status_summary,
3065 })
3066}
3067
3068fn external_upgrade_verification_check_digest(
3069 check: &ExternalUpgradeVerificationCheckV1,
3070) -> String {
3071 stable_json_sha256_hex(&ExternalUpgradeVerificationCheckDigestInput {
3072 check_id: &check.check_id,
3073 policy_id: &check.policy_id,
3074 policy_digest: &check.policy_digest,
3075 proposal_id: &check.proposal_id,
3076 proposal_digest: &check.proposal_digest,
3077 subject: &check.subject,
3078 canister_id: &check.canister_id,
3079 role: &check.role,
3080 observation: &check.observation,
3081 requirement_results: &check.requirement_results,
3082 verification_result: check.verification_result,
3083 status_summary: &check.status_summary,
3084 })
3085}
3086
3087fn external_upgrade_completion_report_digest(report: &ExternalUpgradeCompletionReportV1) -> String {
3088 stable_json_sha256_hex(&ExternalUpgradeCompletionReportDigestInput {
3089 report_id: &report.report_id,
3090 proposal_id: &report.proposal_id,
3091 proposal_digest: &report.proposal_digest,
3092 consent_evidence_id: &report.consent_evidence_id,
3093 consent_evidence_digest: &report.consent_evidence_digest,
3094 verification_check_id: &report.verification_check_id,
3095 verification_check_digest: &report.verification_check_digest,
3096 subject: &report.subject,
3097 canister_id: &report.canister_id,
3098 role: &report.role,
3099 consent_state: report.consent_state,
3100 verification_result: report.verification_result,
3101 completion_status: report.completion_status,
3102 blockers: &report.blockers,
3103 next_actions: &report.next_actions,
3104 status_summary: &report.status_summary,
3105 })
3106}
3107
3108fn observed_before_digest(
3109 authority: &LifecycleAuthorityV1,
3110 current_module_hash: Option<&String>,
3111 current_config_hash: Option<&String>,
3112) -> String {
3113 stable_json_sha256_hex(&ObservedBeforeDigestInput {
3114 subject: &authority.subject,
3115 canister_id: &authority.canister_id,
3116 role: &authority.role,
3117 observed_controllers: &authority.observed_controllers,
3118 current_module_hash,
3119 current_canonical_embedded_config_sha256: current_config_hash,
3120 })
3121}
3122
3123fn external_upgrade_verification_result(
3124 consent_state: ExternalUpgradeConsentStateV1,
3125 proposal: &ExternalUpgradeProposalV1,
3126 observed_after_module_hash: Option<&str>,
3127 observed_after_config: Option<&str>,
3128) -> ExternalUpgradeVerificationResultV1 {
3129 match consent_state {
3130 ExternalUpgradeConsentStateV1::Pending => ExternalUpgradeVerificationResultV1::Pending,
3131 ExternalUpgradeConsentStateV1::Refused => ExternalUpgradeVerificationResultV1::Refused,
3132 ExternalUpgradeConsentStateV1::Delegated
3133 | ExternalUpgradeConsentStateV1::ExecutedExternally => {
3134 if external_upgrade_observation_matches(
3135 proposal.target_installed_module_hash.as_deref(),
3136 observed_after_module_hash,
3137 ) && external_upgrade_observation_matches(
3138 proposal.target_canonical_embedded_config_sha256.as_deref(),
3139 observed_after_config,
3140 ) {
3141 ExternalUpgradeVerificationResultV1::Verified
3142 } else {
3143 ExternalUpgradeVerificationResultV1::Mismatch
3144 }
3145 }
3146 }
3147}
3148
3149fn external_upgrade_verification_notes(
3150 verification_result: ExternalUpgradeVerificationResultV1,
3151 proposal: &ExternalUpgradeProposalV1,
3152 observed_after_module_hash: Option<&str>,
3153 observed_after_config: Option<&str>,
3154) -> Vec<String> {
3155 let mut notes = Vec::new();
3156 if verification_result == ExternalUpgradeVerificationResultV1::Mismatch {
3157 if !external_upgrade_observation_matches(
3158 proposal.target_installed_module_hash.as_deref(),
3159 observed_after_module_hash,
3160 ) {
3161 notes.push("observed module hash does not match proposal target".to_string());
3162 }
3163 if !external_upgrade_observation_matches(
3164 proposal.target_canonical_embedded_config_sha256.as_deref(),
3165 observed_after_config,
3166 ) {
3167 notes.push("observed embedded config does not match proposal target".to_string());
3168 }
3169 }
3170 notes
3171}
3172
3173fn external_lifecycle_check_summary(
3174 status: ExternalLifecyclePlanStatusV1,
3175 pending_report: &ExternalLifecyclePendingReportV1,
3176) -> String {
3177 match status {
3178 ExternalLifecyclePlanStatusV1::Ready => {
3179 format!(
3180 "external lifecycle is ready: {} directly executable role(s), no pending external action",
3181 pending_report.direct_upgrade_count
3182 )
3183 }
3184 ExternalLifecyclePlanStatusV1::PendingExternalAction => {
3185 format!(
3186 "external lifecycle has {} pending external action(s) and {} directly executable role(s)",
3187 pending_report.pending_external_count, pending_report.direct_upgrade_count
3188 )
3189 }
3190 ExternalLifecyclePlanStatusV1::Blocked => {
3191 format!(
3192 "external lifecycle is blocked by {} role/canister subject(s)",
3193 pending_report.blocked_count
3194 )
3195 }
3196 }
3197}
3198
3199fn external_lifecycle_check_next_actions(
3200 status: ExternalLifecyclePlanStatusV1,
3201 pending_report: &ExternalLifecyclePendingReportV1,
3202) -> Vec<String> {
3203 match status {
3204 ExternalLifecyclePlanStatusV1::Ready => {
3205 vec!["continue through the normal guarded deployment path".to_string()]
3206 }
3207 ExternalLifecyclePlanStatusV1::PendingExternalAction => pending_report
3208 .pending_external_actions
3209 .iter()
3210 .map(|action| {
3211 format!(
3212 "request {} for {}",
3213 action.required_external_action, action.subject
3214 )
3215 })
3216 .collect(),
3217 ExternalLifecyclePlanStatusV1::Blocked => {
3218 vec!["resolve blocked external lifecycle subjects before execution".to_string()]
3219 }
3220 }
3221}
3222
3223fn external_lifecycle_handoff_summary(report: &ExternalLifecyclePendingReportV1) -> String {
3224 match report.status {
3225 ExternalLifecyclePlanStatusV1::Ready => {
3226 "no external lifecycle handoff is required".to_string()
3227 }
3228 ExternalLifecyclePlanStatusV1::PendingExternalAction => format!(
3229 "{} external lifecycle handoff action(s) require operator coordination",
3230 report.pending_external_count
3231 ),
3232 ExternalLifecyclePlanStatusV1::Blocked => format!(
3233 "external lifecycle handoff is blocked by {} subject(s)",
3234 report.blocked_count
3235 ),
3236 }
3237}
3238
3239fn external_lifecycle_handoff_instructions(proposal: &ExternalUpgradeProposalV1) -> Vec<String> {
3240 let mut instructions = vec![
3241 format!(
3242 "present proposal {} for subject {}",
3243 proposal.proposal_id, proposal.subject
3244 ),
3245 "verify live inventory after any reported external action".to_string(),
3246 ];
3247 if let Some(expires_at) = proposal.expires_at.as_deref() {
3248 instructions.push(format!("do not use this proposal after {expires_at}"));
3249 }
3250 match proposal.lifecycle_mode {
3251 LifecycleModeV1::ProposalRequired => {
3252 instructions.push("collect explicit consent before direct install".to_string());
3253 }
3254 LifecycleModeV1::DelegatedInstallRequired => {
3255 instructions.push("use delegated install authority only if policy allows".to_string());
3256 }
3257 LifecycleModeV1::ExternalCompletionOnly | LifecycleModeV1::VerifyOnly => {
3258 instructions
3259 .push("wait for external completion evidence before verification".to_string());
3260 }
3261 LifecycleModeV1::MustNotTouch | LifecycleModeV1::UnknownUnsafeBlocked => {
3262 instructions.push("do not execute; report blocked lifecycle state".to_string());
3263 }
3264 LifecycleModeV1::DirectDeploymentAuthority => {
3265 instructions.push("no external handoff should be required".to_string());
3266 }
3267 }
3268 instructions
3269}
3270
3271const fn external_upgrade_verification_summary(
3272 result: ExternalUpgradeVerificationResultV1,
3273) -> &'static str {
3274 match result {
3275 ExternalUpgradeVerificationResultV1::Pending => {
3276 "external action has not been reported as complete"
3277 }
3278 ExternalUpgradeVerificationResultV1::Refused => "external consent was refused",
3279 ExternalUpgradeVerificationResultV1::Verified => {
3280 "reported external completion matches proposal target facts"
3281 }
3282 ExternalUpgradeVerificationResultV1::Mismatch => {
3283 "reported external completion does not match proposal target facts"
3284 }
3285 }
3286}
3287
3288fn external_upgrade_verification_policy_summary(
3289 proposal: &ExternalUpgradeProposalV1,
3290) -> &'static str {
3291 if proposal
3292 .verification_requirements
3293 .contains(&LifecycleVerificationRequirementV1::ProtectedCallReadiness)
3294 {
3295 "fresh live inventory, module/config facts, controller observation, and protected-call readiness are required"
3296 } else {
3297 "fresh live inventory, module/config facts, and controller observation are required"
3298 }
3299}
3300
3301fn external_upgrade_verification_policy_requirements(
3302 proposal: &ExternalUpgradeProposalV1,
3303) -> Vec<ExternalUpgradeVerificationPolicyRequirementV1> {
3304 [
3305 (LifecycleVerificationRequirementV1::LiveInventory, None),
3306 (
3307 LifecycleVerificationRequirementV1::ControllerObservation,
3308 None,
3309 ),
3310 (
3311 LifecycleVerificationRequirementV1::ModuleHash,
3312 proposal.target_installed_module_hash.clone(),
3313 ),
3314 (
3315 LifecycleVerificationRequirementV1::CanonicalEmbeddedConfig,
3316 proposal.target_canonical_embedded_config_sha256.clone(),
3317 ),
3318 (
3319 LifecycleVerificationRequirementV1::ProtectedCallReadiness,
3320 None,
3321 ),
3322 ]
3323 .into_iter()
3324 .map(
3325 |(requirement, expected_value)| ExternalUpgradeVerificationPolicyRequirementV1 {
3326 requirement,
3327 status: if proposal.verification_requirements.contains(&requirement) {
3328 ExternalUpgradeVerificationRequirementStatusV1::Required
3329 } else {
3330 ExternalUpgradeVerificationRequirementStatusV1::NotRequired
3331 },
3332 expected_value,
3333 },
3334 )
3335 .collect()
3336}
3337
3338fn external_upgrade_verification_check_requirements(
3339 policy: &ExternalUpgradeVerificationPolicyV1,
3340 observation: &ExternalUpgradeVerificationObservationV1,
3341) -> Vec<ExternalUpgradeVerificationCheckRequirementV1> {
3342 policy
3343 .verification_requirements
3344 .iter()
3345 .map(|row| {
3346 let observed_value =
3347 external_upgrade_verification_observed_value(row.requirement, observation);
3348 let satisfied =
3349 if row.status == ExternalUpgradeVerificationRequirementStatusV1::Required {
3350 Some(external_upgrade_verification_requirement_satisfied(
3351 row.requirement,
3352 row.expected_value.as_deref(),
3353 observed_value.as_deref(),
3354 observation,
3355 ))
3356 } else {
3357 None
3358 };
3359 ExternalUpgradeVerificationCheckRequirementV1 {
3360 requirement: row.requirement,
3361 status: row.status,
3362 expected_value: row.expected_value.clone(),
3363 observed_value,
3364 satisfied,
3365 }
3366 })
3367 .collect()
3368}
3369
3370fn external_upgrade_verification_observed_value(
3371 requirement: LifecycleVerificationRequirementV1,
3372 observation: &ExternalUpgradeVerificationObservationV1,
3373) -> Option<String> {
3374 match requirement {
3375 LifecycleVerificationRequirementV1::LiveInventory => {
3376 Some(observation.live_inventory_observed.to_string())
3377 }
3378 LifecycleVerificationRequirementV1::ControllerObservation => {
3379 Some(observation.controller_observation_present.to_string())
3380 }
3381 LifecycleVerificationRequirementV1::ModuleHash => observation.observed_module_hash.clone(),
3382 LifecycleVerificationRequirementV1::CanonicalEmbeddedConfig => observation
3383 .observed_canonical_embedded_config_sha256
3384 .clone(),
3385 LifecycleVerificationRequirementV1::ProtectedCallReadiness => observation
3386 .protected_call_ready
3387 .map(|value| value.to_string()),
3388 }
3389}
3390
3391fn external_upgrade_verification_requirement_satisfied(
3392 requirement: LifecycleVerificationRequirementV1,
3393 expected_value: Option<&str>,
3394 observed_value: Option<&str>,
3395 observation: &ExternalUpgradeVerificationObservationV1,
3396) -> bool {
3397 match requirement {
3398 LifecycleVerificationRequirementV1::LiveInventory => observation.live_inventory_observed,
3399 LifecycleVerificationRequirementV1::ControllerObservation => {
3400 observation.controller_observation_present
3401 }
3402 LifecycleVerificationRequirementV1::ModuleHash
3403 | LifecycleVerificationRequirementV1::CanonicalEmbeddedConfig => {
3404 expected_value.is_some_and(|expected| observed_value == Some(expected))
3405 }
3406 LifecycleVerificationRequirementV1::ProtectedCallReadiness => {
3407 observation.protected_call_ready == Some(true)
3408 }
3409 }
3410}
3411
3412fn external_upgrade_verification_check_result(
3413 requirements: &[ExternalUpgradeVerificationCheckRequirementV1],
3414) -> ExternalUpgradeVerificationResultV1 {
3415 if requirements
3416 .iter()
3417 .filter(|row| row.status == ExternalUpgradeVerificationRequirementStatusV1::Required)
3418 .all(|row| row.satisfied == Some(true))
3419 {
3420 ExternalUpgradeVerificationResultV1::Verified
3421 } else {
3422 ExternalUpgradeVerificationResultV1::Mismatch
3423 }
3424}
3425
3426fn validate_external_upgrade_verification_check_requirements(
3427 requirements: &[ExternalUpgradeVerificationCheckRequirementV1],
3428 result: ExternalUpgradeVerificationResultV1,
3429) -> Result<(), ExternalUpgradeVerificationCheckError> {
3430 if requirements.is_empty() {
3431 return Err(
3432 ExternalUpgradeVerificationCheckError::MissingRequiredField {
3433 field: "requirement_results",
3434 },
3435 );
3436 }
3437 let mut seen = BTreeSet::new();
3438 for row in requirements {
3439 if !seen.insert(row.requirement) {
3440 return Err(
3441 ExternalUpgradeVerificationCheckError::DuplicateRequirement {
3442 requirement: row.requirement,
3443 },
3444 );
3445 }
3446 match row.status {
3447 ExternalUpgradeVerificationRequirementStatusV1::Required => {
3448 if row.satisfied.is_none() {
3449 return Err(
3450 ExternalUpgradeVerificationCheckError::RequirementStatusMismatch {
3451 requirement: row.requirement,
3452 },
3453 );
3454 }
3455 }
3456 ExternalUpgradeVerificationRequirementStatusV1::NotRequired => {
3457 if row.satisfied.is_some() {
3458 return Err(
3459 ExternalUpgradeVerificationCheckError::RequirementStatusMismatch {
3460 requirement: row.requirement,
3461 },
3462 );
3463 }
3464 }
3465 }
3466 }
3467 if external_upgrade_verification_check_result(requirements) != result {
3468 return Err(ExternalUpgradeVerificationCheckError::SourceMismatch {
3469 field: "verification_result",
3470 });
3471 }
3472 Ok(())
3473}
3474
3475const fn external_upgrade_completion_status(
3476 consent_state: ExternalUpgradeConsentStateV1,
3477 verification_result: ExternalUpgradeVerificationResultV1,
3478) -> ExternalUpgradeCompletionStatusV1 {
3479 match consent_state {
3480 ExternalUpgradeConsentStateV1::Pending => {
3481 ExternalUpgradeCompletionStatusV1::AwaitingConsent
3482 }
3483 ExternalUpgradeConsentStateV1::Refused => ExternalUpgradeCompletionStatusV1::ConsentRefused,
3484 ExternalUpgradeConsentStateV1::Delegated
3485 | ExternalUpgradeConsentStateV1::ExecutedExternally => match verification_result {
3486 ExternalUpgradeVerificationResultV1::Verified => {
3487 ExternalUpgradeCompletionStatusV1::VerifiedComplete
3488 }
3489 ExternalUpgradeVerificationResultV1::Mismatch => {
3490 ExternalUpgradeCompletionStatusV1::VerificationFailed
3491 }
3492 ExternalUpgradeVerificationResultV1::Pending
3493 | ExternalUpgradeVerificationResultV1::Refused => {
3494 ExternalUpgradeCompletionStatusV1::AwaitingVerification
3495 }
3496 },
3497 }
3498}
3499
3500fn external_upgrade_completion_blockers(status: ExternalUpgradeCompletionStatusV1) -> Vec<String> {
3501 match status {
3502 ExternalUpgradeCompletionStatusV1::AwaitingConsent => {
3503 vec!["external consent or action has not been reported".to_string()]
3504 }
3505 ExternalUpgradeCompletionStatusV1::ConsentRefused => {
3506 vec!["external consent was refused".to_string()]
3507 }
3508 ExternalUpgradeCompletionStatusV1::AwaitingVerification => {
3509 vec!["external action requires verification against live inventory".to_string()]
3510 }
3511 ExternalUpgradeCompletionStatusV1::VerificationFailed => {
3512 vec!["supplied observation does not satisfy verification policy".to_string()]
3513 }
3514 ExternalUpgradeCompletionStatusV1::VerifiedComplete => Vec::new(),
3515 }
3516}
3517
3518fn external_upgrade_completion_next_actions(
3519 status: ExternalUpgradeCompletionStatusV1,
3520) -> Vec<String> {
3521 match status {
3522 ExternalUpgradeCompletionStatusV1::AwaitingConsent => {
3523 vec!["obtain external consent or reported external execution".to_string()]
3524 }
3525 ExternalUpgradeCompletionStatusV1::ConsentRefused => {
3526 vec!["do not execute; supersede the proposal before retry".to_string()]
3527 }
3528 ExternalUpgradeCompletionStatusV1::AwaitingVerification => {
3529 vec!["collect fresh inventory observations and run verification check".to_string()]
3530 }
3531 ExternalUpgradeCompletionStatusV1::VerificationFailed => {
3532 vec!["resolve observed module/config/readiness mismatch".to_string()]
3533 }
3534 ExternalUpgradeCompletionStatusV1::VerifiedComplete => {
3535 vec!["record external lifecycle item as verified complete".to_string()]
3536 }
3537 }
3538}
3539
3540const fn external_upgrade_completion_summary(
3541 status: ExternalUpgradeCompletionStatusV1,
3542) -> &'static str {
3543 match status {
3544 ExternalUpgradeCompletionStatusV1::AwaitingConsent => {
3545 "external lifecycle item is waiting for consent or external action"
3546 }
3547 ExternalUpgradeCompletionStatusV1::ConsentRefused => "external lifecycle item was refused",
3548 ExternalUpgradeCompletionStatusV1::AwaitingVerification => {
3549 "external lifecycle item needs verification before completion"
3550 }
3551 ExternalUpgradeCompletionStatusV1::VerifiedComplete => {
3552 "external lifecycle item is structurally verified complete"
3553 }
3554 ExternalUpgradeCompletionStatusV1::VerificationFailed => {
3555 "external lifecycle item failed supplied verification"
3556 }
3557 }
3558}
3559
3560const fn external_upgrade_verification_check_summary(
3561 result: ExternalUpgradeVerificationResultV1,
3562) -> &'static str {
3563 match result {
3564 ExternalUpgradeVerificationResultV1::Verified => {
3565 "supplied observation satisfies required verification postconditions"
3566 }
3567 ExternalUpgradeVerificationResultV1::Mismatch => {
3568 "supplied observation does not satisfy required verification postconditions"
3569 }
3570 ExternalUpgradeVerificationResultV1::Pending => {
3571 "external verification check is pending observation"
3572 }
3573 ExternalUpgradeVerificationResultV1::Refused => {
3574 "external verification check reflects refused consent"
3575 }
3576 }
3577}
3578
3579const fn external_upgrade_consent_summary(state: ExternalUpgradeConsentStateV1) -> &'static str {
3580 match state {
3581 ExternalUpgradeConsentStateV1::Pending => {
3582 "external consent or action has not been reported"
3583 }
3584 ExternalUpgradeConsentStateV1::Refused => "external consent was refused",
3585 ExternalUpgradeConsentStateV1::Delegated => "delegated install authority was reported",
3586 ExternalUpgradeConsentStateV1::ExecutedExternally => {
3587 "external controller execution was reported"
3588 }
3589 }
3590}
3591
3592fn external_upgrade_observation_matches(expected: Option<&str>, observed: Option<&str>) -> bool {
3593 expected.is_none_or(|expected| observed == Some(expected))
3594}
3595
3596fn ensure_external_receipt_field(
3597 field: &'static str,
3598 value: &str,
3599) -> Result<(), ExternalUpgradeReceiptError> {
3600 if value.trim().is_empty() {
3601 return Err(ExternalUpgradeReceiptError::MissingRequiredField { field });
3602 }
3603 Ok(())
3604}
3605
3606fn ensure_external_receipt_matches_proposal(
3607 field: &'static str,
3608 actual: &str,
3609 expected: &str,
3610) -> Result<(), ExternalUpgradeReceiptError> {
3611 if actual != expected {
3612 return Err(ExternalUpgradeReceiptError::SourceMismatch { field });
3613 }
3614 Ok(())
3615}
3616
3617fn ensure_external_receipt_option_matches_proposal(
3618 field: &'static str,
3619 actual: Option<&str>,
3620 expected: Option<&str>,
3621) -> Result<(), ExternalUpgradeReceiptError> {
3622 if actual != expected {
3623 return Err(ExternalUpgradeReceiptError::SourceMismatch { field });
3624 }
3625 Ok(())
3626}
3627
3628fn ensure_external_consent_evidence_field(
3629 field: &'static str,
3630 value: &str,
3631) -> Result<(), ExternalUpgradeConsentEvidenceError> {
3632 if value.trim().is_empty() {
3633 return Err(ExternalUpgradeConsentEvidenceError::MissingRequiredField { field });
3634 }
3635 Ok(())
3636}
3637
3638fn ensure_external_verification_report_field(
3639 field: &'static str,
3640 value: &str,
3641) -> Result<(), ExternalUpgradeVerificationReportError> {
3642 if value.trim().is_empty() {
3643 return Err(ExternalUpgradeVerificationReportError::MissingRequiredField { field });
3644 }
3645 Ok(())
3646}
3647
3648fn ensure_external_verification_policy_field(
3649 field: &'static str,
3650 value: &str,
3651) -> Result<(), ExternalUpgradeVerificationPolicyError> {
3652 if value.trim().is_empty() {
3653 return Err(ExternalUpgradeVerificationPolicyError::MissingRequiredField { field });
3654 }
3655 Ok(())
3656}
3657
3658fn ensure_external_verification_check_field(
3659 field: &'static str,
3660 value: &str,
3661) -> Result<(), ExternalUpgradeVerificationCheckError> {
3662 if value.trim().is_empty() {
3663 return Err(ExternalUpgradeVerificationCheckError::MissingRequiredField { field });
3664 }
3665 Ok(())
3666}
3667
3668fn ensure_external_completion_report_field(
3669 field: &'static str,
3670 value: &str,
3671) -> Result<(), ExternalUpgradeCompletionReportError> {
3672 if value.trim().is_empty() {
3673 return Err(ExternalUpgradeCompletionReportError::MissingRequiredField { field });
3674 }
3675 Ok(())
3676}
3677
3678fn ensure_completion_sources_match_proposal(
3679 proposal: &ExternalUpgradeProposalV1,
3680 consent_evidence: &ExternalUpgradeConsentEvidenceV1,
3681 verification_check: &ExternalUpgradeVerificationCheckV1,
3682) -> Result<(), ExternalUpgradeCompletionReportError> {
3683 ensure_completion_source_field(
3684 "consent_evidence.proposal_id",
3685 consent_evidence.proposal_id.as_str(),
3686 proposal.proposal_id.as_str(),
3687 )?;
3688 ensure_completion_source_field(
3689 "consent_evidence.proposal_digest",
3690 consent_evidence.proposal_digest.as_str(),
3691 proposal.proposal_digest.as_str(),
3692 )?;
3693 ensure_completion_source_field(
3694 "verification_check.proposal_id",
3695 verification_check.proposal_id.as_str(),
3696 proposal.proposal_id.as_str(),
3697 )?;
3698 ensure_completion_source_field(
3699 "verification_check.proposal_digest",
3700 verification_check.proposal_digest.as_str(),
3701 proposal.proposal_digest.as_str(),
3702 )?;
3703 ensure_completion_source_field(
3704 "consent_evidence.subject",
3705 consent_evidence.subject.as_str(),
3706 proposal.subject.as_str(),
3707 )?;
3708 ensure_completion_source_field(
3709 "verification_check.subject",
3710 verification_check.subject.as_str(),
3711 proposal.subject.as_str(),
3712 )?;
3713 ensure_completion_option_source_field(
3714 "consent_evidence.canister_id",
3715 consent_evidence.canister_id.as_deref(),
3716 proposal.canister_id.as_deref(),
3717 )?;
3718 ensure_completion_option_source_field(
3719 "verification_check.canister_id",
3720 verification_check.canister_id.as_deref(),
3721 proposal.canister_id.as_deref(),
3722 )?;
3723 ensure_completion_option_source_field(
3724 "consent_evidence.role",
3725 consent_evidence.role.as_deref(),
3726 proposal.role.as_deref(),
3727 )?;
3728 ensure_completion_option_source_field(
3729 "verification_check.role",
3730 verification_check.role.as_deref(),
3731 proposal.role.as_deref(),
3732 )
3733}
3734
3735fn ensure_completion_source_field(
3736 field: &'static str,
3737 actual: &str,
3738 expected: &str,
3739) -> Result<(), ExternalUpgradeCompletionReportError> {
3740 if actual != expected {
3741 return Err(ExternalUpgradeCompletionReportError::SourceMismatch { field });
3742 }
3743 Ok(())
3744}
3745
3746fn ensure_completion_option_source_field(
3747 field: &'static str,
3748 actual: Option<&str>,
3749 expected: Option<&str>,
3750) -> Result<(), ExternalUpgradeCompletionReportError> {
3751 if actual != expected {
3752 return Err(ExternalUpgradeCompletionReportError::SourceMismatch { field });
3753 }
3754 Ok(())
3755}
3756
3757fn ensure_external_lifecycle_plan_field(
3758 field: &'static str,
3759 value: &str,
3760) -> Result<(), ExternalLifecyclePlanError> {
3761 if value.trim().is_empty() {
3762 return Err(ExternalLifecyclePlanError::MissingRequiredField { field });
3763 }
3764 Ok(())
3765}
3766
3767fn ensure_external_proposal_report_field(
3768 field: &'static str,
3769 value: &str,
3770) -> Result<(), ExternalUpgradeProposalReportError> {
3771 if value.trim().is_empty() {
3772 return Err(ExternalUpgradeProposalReportError::MissingRequiredField { field });
3773 }
3774 Ok(())
3775}
3776
3777fn ensure_external_pending_report_field(
3778 field: &'static str,
3779 value: &str,
3780) -> Result<(), ExternalLifecyclePendingReportError> {
3781 if value.trim().is_empty() {
3782 return Err(ExternalLifecyclePendingReportError::MissingRequiredField { field });
3783 }
3784 Ok(())
3785}
3786
3787fn ensure_external_lifecycle_check_field(
3788 field: &'static str,
3789 value: &str,
3790) -> Result<(), ExternalLifecycleCheckError> {
3791 if value.trim().is_empty() {
3792 return Err(ExternalLifecycleCheckError::MissingRequiredField { field });
3793 }
3794 Ok(())
3795}
3796
3797fn ensure_external_lifecycle_handoff_field(
3798 field: &'static str,
3799 value: &str,
3800) -> Result<(), ExternalLifecycleHandoffError> {
3801 if value.trim().is_empty() {
3802 return Err(ExternalLifecycleHandoffError::MissingRequiredField { field });
3803 }
3804 Ok(())
3805}
3806
3807fn ensure_critical_fix_report_field(
3808 field: &'static str,
3809 value: &str,
3810) -> Result<(), CriticalExternalFixReportError> {
3811 if value.trim().is_empty() {
3812 return Err(CriticalExternalFixReportError::MissingRequiredField { field });
3813 }
3814 Ok(())
3815}
3816
3817fn ensure_lifecycle_authority_report_field(
3818 field: &'static str,
3819 value: &str,
3820) -> Result<(), LifecycleAuthorityReportError> {
3821 if value.trim().is_empty() {
3822 return Err(LifecycleAuthorityReportError::MissingRequiredField { field });
3823 }
3824 Ok(())
3825}
3826
3827fn sorted_unique(values: Vec<String>) -> Vec<String> {
3828 values
3829 .into_iter()
3830 .collect::<BTreeSet<_>>()
3831 .into_iter()
3832 .collect()
3833}