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