1use crate::canonical::{hex_lower, is_lower_hex};
12use crate::model::{
13 BASELINE_CLOCK_HZ, MAXIMUM_DSPS, MAXIMUM_LUTS, MAXIMUM_REGISTERS, PERFORMANCE_PROFILE,
14 PromotionRequest, RequestFileLocator, validate_absolute_path,
15};
16use crate::{FailureCode, PromotionError, PromotionResult};
17use sha2::{Digest as _, Sha256};
18use std::collections::BTreeSet;
19
20const ROUTE_ADAPTATION_STATUS: &str = "adaptation-required";
21const ACCEPTANCE_EVIDENCE_TIER: &str = "vitis-link-route";
22const ACCEPTANCE_CAPTURE_KIND: &str = "acceptance-link";
23const ACCEPTANCE_STATUS: &str = "pass";
24const REQUIRED_AMD_RELEASE: &str = "2023.2";
25const REQUIRED_PLATFORM: &str = "xilinx_u280_gen3x16_xdma_1_202211_1";
26const REQUIRED_DEVICE_PART: &str = "xcu280-fsvh2892-2L-e";
27const REQUIRED_XCLBIN_TARGET: &str = "hw";
28const REPORT_SET_DIGEST_DOMAIN: &[u8] = b"hashsigs-u280-route-report-set-v1\0";
29const XCLBIN_IDENTITY_DIGEST_DOMAIN: &[u8] = b"hashsigs-u280-route-xclbin-identity-v1\0";
30const MAXIMUM_RAW_ROUTE_REPORTS: usize = 100_000;
34
35#[derive(Debug)]
69pub struct AcceptedRouteEvidence {
70 policy: ValidatedRoutePolicy,
71 #[allow(dead_code)] retained_descriptors: RetainedRouteDescriptorGuard,
73}
74
75#[derive(Debug)]
77struct ValidatedRoutePolicy {
78 design_digest: String,
79 actual_routed_hz: u64,
80 setup_wns_femtoseconds: i64,
81 hold_whs_femtoseconds: i64,
82 whole_design_luts: u64,
83 whole_design_registers: u64,
84 whole_design_dsps: u64,
85 stage_manifest: RouteArtifactBinding,
86 run_inventory: RouteArtifactBinding,
87 route_seal: RouteArtifactBinding,
88 raw_reports: Vec<RouteArtifactBinding>,
89 routed_clock_evidence: RouteArtifactBinding,
90 xclbin_identity: RouteXclbinIdentity,
91}
92
93#[derive(Debug)]
95enum RetainedRouteDescriptorGuard {}
96
97impl AcceptedRouteEvidence {
98 #[must_use]
100 pub fn design_digest(&self) -> &str {
101 &self.policy.design_digest
102 }
103
104 #[must_use]
106 pub const fn actual_routed_hz(&self) -> u64 {
107 self.policy.actual_routed_hz
108 }
109
110 #[must_use]
112 pub const fn setup_wns_femtoseconds(&self) -> i64 {
113 self.policy.setup_wns_femtoseconds
114 }
115
116 #[must_use]
118 pub const fn hold_whs_femtoseconds(&self) -> i64 {
119 self.policy.hold_whs_femtoseconds
120 }
121
122 #[must_use]
124 pub const fn whole_design_luts(&self) -> u64 {
125 self.policy.whole_design_luts
126 }
127
128 #[must_use]
130 pub const fn whole_design_registers(&self) -> u64 {
131 self.policy.whole_design_registers
132 }
133
134 #[must_use]
136 pub const fn whole_design_dsps(&self) -> u64 {
137 self.policy.whole_design_dsps
138 }
139
140 #[allow(dead_code)] pub(crate) const fn stage_manifest(&self) -> &RouteArtifactBinding {
142 &self.policy.stage_manifest
143 }
144
145 #[allow(dead_code)] pub(crate) const fn run_inventory(&self) -> &RouteArtifactBinding {
147 &self.policy.run_inventory
148 }
149
150 #[allow(dead_code)] pub(crate) const fn route_seal(&self) -> &RouteArtifactBinding {
152 &self.policy.route_seal
153 }
154
155 #[allow(dead_code)] pub(crate) fn raw_reports(&self) -> &[RouteArtifactBinding] {
157 &self.policy.raw_reports
158 }
159
160 #[allow(dead_code)] pub(crate) const fn routed_clock_evidence(&self) -> &RouteArtifactBinding {
162 &self.policy.routed_clock_evidence
163 }
164
165 #[allow(dead_code)] pub(crate) const fn xclbin(&self) -> &RouteArtifactBinding {
167 &self.policy.xclbin_identity.subject
168 }
169
170 #[allow(dead_code)] pub(crate) const fn xclbin_identity(&self) -> &RouteXclbinIdentity {
172 &self.policy.xclbin_identity
173 }
174}
175
176pub fn require_accepted_route(
186 _request: &PromotionRequest,
187) -> PromotionResult<AcceptedRouteEvidence> {
188 Err(PromotionError::new(
189 FailureCode::RouteUnaccepted,
190 format!(
191 "Vitis route evidence status is {ROUTE_ADAPTATION_STATUS}; exact accepted-route parser is unavailable"
192 ),
193 ))
194}
195
196#[derive(Debug, Eq, PartialEq)]
202pub(crate) struct RouteArtifactBinding {
203 absolute_path: String,
204 bytes: u64,
205 sha256: String,
206 descriptor: ParserDescriptorSubject,
207}
208
209#[derive(Debug, Eq, PartialEq)]
211struct ParserDescriptorSubject {
212 canonical_realpath: String,
213 device: u64,
214 inode: u64,
215}
216
217impl RouteArtifactBinding {
218 #[allow(dead_code)] fn from_exact_parser(
220 absolute_path: String,
221 canonical_realpath: String,
222 bytes: u64,
223 sha256: String,
224 device: u64,
225 inode: u64,
226 ) -> Self {
227 Self {
228 absolute_path,
229 bytes,
230 sha256,
231 descriptor: ParserDescriptorSubject {
232 canonical_realpath,
233 device,
234 inode,
235 },
236 }
237 }
238
239 pub(crate) fn absolute_path(&self) -> &str {
240 &self.absolute_path
241 }
242
243 #[allow(dead_code)] pub(crate) fn canonical_realpath(&self) -> &str {
245 &self.descriptor.canonical_realpath
246 }
247
248 #[allow(dead_code)] pub(crate) const fn bytes(&self) -> u64 {
250 self.bytes
251 }
252
253 #[allow(dead_code)] pub(crate) fn sha256(&self) -> &str {
255 &self.sha256
256 }
257
258 #[allow(dead_code)] pub(crate) const fn device(&self) -> u64 {
260 self.descriptor.device
261 }
262
263 #[allow(dead_code)] pub(crate) const fn inode(&self) -> u64 {
265 self.descriptor.inode
266 }
267
268 fn validate(&self, label: &str) -> PromotionResult<()> {
269 validate_absolute_path(&self.absolute_path, label)?;
270 validate_absolute_path(&self.descriptor.canonical_realpath, label)?;
271 if self.absolute_path != self.descriptor.canonical_realpath {
272 return Err(PromotionError::new(
273 FailureCode::IdentityJoin,
274 format!("{label} pathname differs from its descriptor-derived canonical realpath"),
275 ));
276 }
277 if self.descriptor.inode == 0 {
278 return Err(PromotionError::new(
279 FailureCode::Noncanonical,
280 format!("{label} has an invalid zero descriptor inode"),
281 ));
282 }
283 if self.bytes == 0 || !is_lower_hex(&self.sha256, 64, true) {
284 return Err(PromotionError::new(
285 FailureCode::Noncanonical,
286 format!("{label} has a zero length or non-canonical SHA-256"),
287 ));
288 }
289 Ok(())
290 }
291
292 fn require_request_binding(
293 &self,
294 request: &RequestFileLocator<'_>,
295 label: &str,
296 ) -> PromotionResult<()> {
297 if self.absolute_path != request.absolute_path()
298 || self.bytes != request.bytes()
299 || self.sha256 != request.sha256()
300 {
301 return Err(PromotionError::new(
302 FailureCode::IdentityJoin,
303 format!("{label} differs from the exact promotion-request binding"),
304 ));
305 }
306 Ok(())
307 }
308}
309
310#[derive(Debug)]
317pub(crate) struct RouteXclbinIdentity {
318 subject: RouteArtifactBinding,
319 uuid: String,
320 interface_uuid: String,
321 deployment_platform: String,
322 fpga_part: String,
323 target: String,
324 descriptor_identity_sha256: String,
325}
326
327impl RouteXclbinIdentity {
328 #[allow(dead_code)] fn from_exact_parser(
330 subject: RouteArtifactBinding,
331 uuid: String,
332 interface_uuid: String,
333 deployment_platform: String,
334 fpga_part: String,
335 target: String,
336 ) -> PromotionResult<Self> {
337 let descriptor_identity_sha256 = xclbin_identity_sha256(
338 &subject,
339 &uuid,
340 &interface_uuid,
341 &deployment_platform,
342 &fpga_part,
343 &target,
344 )?;
345 Ok(Self {
346 subject,
347 uuid,
348 interface_uuid,
349 deployment_platform,
350 fpga_part,
351 target,
352 descriptor_identity_sha256,
353 })
354 }
355
356 #[allow(dead_code)] pub(crate) const fn subject(&self) -> &RouteArtifactBinding {
358 &self.subject
359 }
360
361 #[allow(dead_code)] pub(crate) fn uuid(&self) -> &str {
363 &self.uuid
364 }
365
366 #[allow(dead_code)] pub(crate) fn interface_uuid(&self) -> &str {
368 &self.interface_uuid
369 }
370
371 #[allow(dead_code)] pub(crate) fn deployment_platform(&self) -> &str {
373 &self.deployment_platform
374 }
375
376 #[allow(dead_code)] pub(crate) fn fpga_part(&self) -> &str {
378 &self.fpga_part
379 }
380
381 #[allow(dead_code)] pub(crate) fn target(&self) -> &str {
383 &self.target
384 }
385}
386
387#[allow(dead_code)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
389enum ResourceScope {
390 WholeRoutedDesignIncludingShell,
391 KernelHierarchyOnly,
392}
393
394#[derive(Debug)]
395struct RouteIdentityObservation {
396 evidence_tier: String,
397 capture_kind: String,
398 status: String,
399 profile: String,
400 amd_release: String,
401 deployment_platform: String,
402 device_part: String,
403 route_design_digest: String,
404 requested_clock_hz: u64,
405 actual_routed_hz: u64,
406}
407
408#[derive(Debug)]
410struct StageManifestIdentityObservation {
411 source: RouteArtifactBinding,
412 route_provenance: RouteObservationProvenance,
413 stage_subject_sha256: String,
414 design_digest: String,
415}
416
417#[derive(Debug)]
419struct RunInventoryIdentityObservation {
420 source: RouteArtifactBinding,
421 route_provenance: RouteObservationProvenance,
422 stage_subject_sha256: String,
423 design_digest: String,
424}
425
426#[derive(Debug)]
432struct RouteObservationProvenance {
433 run_inventory: RouteArtifactBinding,
434 route_seal: RouteArtifactBinding,
435 raw_report_set_sha256: String,
436 source_report: RouteArtifactBinding,
437}
438
439#[derive(Debug)]
441struct BoundRouteObservation<T> {
442 provenance: RouteObservationProvenance,
443 value: T,
444}
445
446#[allow(clippy::struct_excessive_bools)] #[derive(Debug)]
448struct RouteStatusObservation {
449 placed_fully: bool,
450 routed_fully: bool,
451 partially_routed: bool,
452 route_errors: bool,
453}
454
455#[derive(Debug)]
456struct RouteTimingObservation {
457 setup_wns_femtoseconds: i64,
458 hold_whs_femtoseconds: i64,
459 setup_path_count: u64,
460 hold_path_count: u64,
461 unconstrained_path_count: u64,
462 unconstrained_endpoint_count: u64,
463 complete_timing_coverage: bool,
464}
465
466#[derive(Debug)]
467struct RouteDrcObservation {
468 complete_no_waivers_evaluation: bool,
469 enabled_error_violations: u64,
470 enabled_fatal_violations: u64,
471 installed_waivers: u64,
472}
473
474#[derive(Debug)]
475struct RouteResourceObservation {
476 scope: ResourceScope,
477 luts: u64,
478 registers: u64,
479 dsps: u64,
480 declared_lut_cap: u64,
481 declared_register_cap: u64,
482 declared_dsp_cap: u64,
483}
484
485#[allow(clippy::struct_excessive_bools)] #[derive(Debug)]
487struct RouteNonclaims {
488 hardware_execution: bool,
489 card_validated: bool,
490 cryptographic_outputs_valid: bool,
491 performance_validated: bool,
492 hardware_completion_promotable: bool,
493}
494
495#[derive(Debug)]
502struct RoutePolicyInputs {
503 identity: BoundRouteObservation<RouteIdentityObservation>,
504 stage_identity: StageManifestIdentityObservation,
505 inventory_identity: RunInventoryIdentityObservation,
506 status: BoundRouteObservation<RouteStatusObservation>,
507 timing: BoundRouteObservation<RouteTimingObservation>,
508 drc: BoundRouteObservation<RouteDrcObservation>,
509 resources: BoundRouteObservation<RouteResourceObservation>,
510 nonclaims: RouteNonclaims,
511 stage_manifest: RouteArtifactBinding,
512 run_inventory: RouteArtifactBinding,
513 route_seal: RouteArtifactBinding,
514 inventory_raw_reports: Vec<RouteArtifactBinding>,
515 seal_raw_reports: Vec<RouteArtifactBinding>,
516 routed_clock_evidence: RouteArtifactBinding,
517 xclbin_identity: RouteXclbinIdentity,
518 inventory_routed_clock_evidence: RouteArtifactBinding,
519 inventory_xclbin: RouteArtifactBinding,
520 seal_run_inventory: RouteArtifactBinding,
521 seal_routed_clock_evidence: RouteArtifactBinding,
522 seal_xclbin: RouteArtifactBinding,
523}
524
525#[allow(dead_code, clippy::large_types_passed_by_value, clippy::too_many_lines)] fn validate_route_policy_inputs(
533 request: &PromotionRequest,
534 input: RoutePolicyInputs,
535) -> PromotionResult<ValidatedRoutePolicy> {
536 validate_nonclaims(&input.nonclaims)?;
537
538 for (artifact, label) in [
539 (&input.stage_manifest, "route stage manifest"),
540 (&input.run_inventory, "route run inventory"),
541 (&input.route_seal, "accepted route seal"),
542 (&input.routed_clock_evidence, "routed-clock evidence"),
543 (&input.xclbin_identity.subject, "routed xclbin"),
544 (
545 &input.inventory_routed_clock_evidence,
546 "inventory routed-clock binding",
547 ),
548 (&input.inventory_xclbin, "inventory xclbin binding"),
549 (&input.seal_run_inventory, "route-seal inventory binding"),
550 (
551 &input.seal_routed_clock_evidence,
552 "route-seal routed-clock binding",
553 ),
554 (&input.seal_xclbin, "route-seal xclbin binding"),
555 ] {
556 artifact.validate(label)?;
557 }
558
559 let locators = request.route_locators();
560 input
561 .stage_manifest
562 .require_request_binding(locators.staged_run_manifest(), "route stage manifest")?;
563 input
564 .run_inventory
565 .require_request_binding(locators.complete_run_inventory(), "route run inventory")?;
566 input
567 .route_seal
568 .require_request_binding(locators.accepted_250mhz_route_seal(), "accepted route seal")?;
569 require_strict_child(
570 locators.staged_run_root().absolute_path(),
571 input.stage_manifest.absolute_path(),
572 "route stage manifest",
573 )?;
574 require_strict_child(
575 locators.acceptance_run_root().absolute_path(),
576 input.routed_clock_evidence.absolute_path(),
577 "routed-clock evidence",
578 )?;
579 require_strict_child(
580 locators.acceptance_run_root().absolute_path(),
581 input.xclbin_identity.subject.absolute_path(),
582 "routed xclbin",
583 )?;
584 require_same_artifact(
585 &input.run_inventory,
586 &input.seal_run_inventory,
587 "route seal to run inventory",
588 )?;
589 require_same_artifact(
590 &input.routed_clock_evidence,
591 &input.inventory_routed_clock_evidence,
592 "run inventory to routed-clock evidence",
593 )?;
594 require_same_artifact(
595 &input.routed_clock_evidence,
596 &input.seal_routed_clock_evidence,
597 "route seal to routed-clock evidence",
598 )?;
599 require_same_artifact(
600 &input.xclbin_identity.subject,
601 &input.inventory_xclbin,
602 "run inventory to xclbin",
603 )?;
604 require_same_artifact(
605 &input.xclbin_identity.subject,
606 &input.seal_xclbin,
607 "route seal to xclbin",
608 )?;
609 validate_raw_report_bindings(
610 locators.acceptance_run_root().absolute_path(),
611 &input.inventory_raw_reports,
612 &input.seal_raw_reports,
613 )?;
614
615 let mut paths = BTreeSet::new();
616 let mut physical_subjects = BTreeSet::new();
617 for artifact in [
618 &input.stage_manifest,
619 &input.run_inventory,
620 &input.route_seal,
621 &input.routed_clock_evidence,
622 &input.xclbin_identity.subject,
623 ] {
624 require_unique_artifact_subject(&mut paths, &mut physical_subjects, artifact)?;
625 }
626 for report in &input.inventory_raw_reports {
627 require_unique_artifact_subject(&mut paths, &mut physical_subjects, report)?;
628 }
629
630 let report_set_sha256 = canonical_raw_report_set_sha256(&input.inventory_raw_reports)?;
631 for (provenance, label) in [
632 (&input.identity.provenance, "route identity observation"),
633 (
634 &input.stage_identity.route_provenance,
635 "stage identity observation",
636 ),
637 (
638 &input.inventory_identity.route_provenance,
639 "run-inventory identity observation",
640 ),
641 (&input.status.provenance, "route status observation"),
642 (&input.timing.provenance, "route timing observation"),
643 (&input.drc.provenance, "route DRC observation"),
644 (&input.resources.provenance, "route resource observation"),
645 ] {
646 validate_observation_provenance(
647 provenance,
648 &input.run_inventory,
649 &input.route_seal,
650 &input.inventory_raw_reports,
651 &report_set_sha256,
652 label,
653 )?;
654 }
655
656 validate_identity(&input.identity.value)?;
657 validate_route_status(&input.status.value)?;
658 validate_timing(&input.timing.value)?;
659 validate_drc(&input.drc.value)?;
660 validate_resources(&input.resources.value)?;
661 validate_stage_identity_join(
662 &input.identity.value,
663 &input.stage_identity,
664 &input.inventory_identity,
665 &input.stage_manifest,
666 &input.run_inventory,
667 )?;
668 validate_xclbin_identity(&input.identity.value, &input.xclbin_identity)?;
669
670 Ok(ValidatedRoutePolicy {
671 design_digest: input.identity.value.route_design_digest,
672 actual_routed_hz: input.identity.value.actual_routed_hz,
673 setup_wns_femtoseconds: input.timing.value.setup_wns_femtoseconds,
674 hold_whs_femtoseconds: input.timing.value.hold_whs_femtoseconds,
675 whole_design_luts: input.resources.value.luts,
676 whole_design_registers: input.resources.value.registers,
677 whole_design_dsps: input.resources.value.dsps,
678 stage_manifest: input.stage_manifest,
679 run_inventory: input.run_inventory,
680 route_seal: input.route_seal,
681 raw_reports: input.inventory_raw_reports,
682 routed_clock_evidence: input.routed_clock_evidence,
683 xclbin_identity: input.xclbin_identity,
684 })
685}
686
687fn validate_identity(identity: &RouteIdentityObservation) -> PromotionResult<()> {
688 let fixed_identity = identity.evidence_tier == ACCEPTANCE_EVIDENCE_TIER
689 && identity.capture_kind == ACCEPTANCE_CAPTURE_KIND
690 && identity.status == ACCEPTANCE_STATUS
691 && identity.profile == PERFORMANCE_PROFILE
692 && identity.amd_release == REQUIRED_AMD_RELEASE
693 && identity.deployment_platform == REQUIRED_PLATFORM
694 && identity.device_part == REQUIRED_DEVICE_PART
695 && is_lower_hex(&identity.route_design_digest, 64, true)
696 && identity.requested_clock_hz == BASELINE_CLOCK_HZ;
697 if !fixed_identity {
698 return Err(PromotionError::new(
699 FailureCode::RouteUnaccepted,
700 "route parser output does not identify the exact SHA-256 U280 250 MHz acceptance design",
701 ));
702 }
703 if identity.actual_routed_hz < BASELINE_CLOCK_HZ {
704 return Err(PromotionError::new(
705 FailureCode::RouteTiming,
706 format!(
707 "actual routed clock {} Hz is below the required {} Hz baseline",
708 identity.actual_routed_hz, BASELINE_CLOCK_HZ
709 ),
710 ));
711 }
712 Ok(())
713}
714
715fn validate_stage_identity_join(
716 route: &RouteIdentityObservation,
717 stage: &StageManifestIdentityObservation,
718 inventory: &RunInventoryIdentityObservation,
719 stage_manifest: &RouteArtifactBinding,
720 run_inventory: &RouteArtifactBinding,
721) -> PromotionResult<()> {
722 stage.source.validate("stage identity source")?;
723 inventory.source.validate("run-inventory identity source")?;
724 let canonical = is_lower_hex(&stage.stage_subject_sha256, 64, true)
725 && is_lower_hex(&inventory.stage_subject_sha256, 64, true)
726 && is_lower_hex(&stage.design_digest, 64, true)
727 && is_lower_hex(&inventory.design_digest, 64, true);
728 if !canonical {
729 return Err(PromotionError::new(
730 FailureCode::Noncanonical,
731 "stage-manifest or run-inventory identity contains a non-canonical digest",
732 ));
733 }
734 if &stage.source != stage_manifest
735 || &inventory.source != run_inventory
736 || stage.stage_subject_sha256 != stage_manifest.sha256
737 || inventory.stage_subject_sha256 != stage.stage_subject_sha256
738 || stage.design_digest != route.route_design_digest
739 || inventory.design_digest != stage.design_digest
740 {
741 return Err(PromotionError::new(
742 FailureCode::IdentityJoin,
743 "stage manifest and run inventory do not independently join one stage subject and design family",
744 ));
745 }
746 Ok(())
747}
748
749fn validate_xclbin_identity(
750 route: &RouteIdentityObservation,
751 xclbin: &RouteXclbinIdentity,
752) -> PromotionResult<()> {
753 if !is_canonical_uuid(&xclbin.uuid) || !is_canonical_uuid(&xclbin.interface_uuid) {
754 return Err(PromotionError::new(
755 FailureCode::Noncanonical,
756 "route-derived xclbin UUID or interface UUID is not canonical",
757 ));
758 }
759 if xclbin.deployment_platform != route.deployment_platform
760 || xclbin.fpga_part != route.device_part
761 {
762 return Err(PromotionError::new(
763 FailureCode::IdentityJoin,
764 "route-derived xclbin platform or FPGA part differs from the accepted route",
765 ));
766 }
767 if xclbin.target != REQUIRED_XCLBIN_TARGET {
768 return Err(PromotionError::new(
769 FailureCode::RouteUnaccepted,
770 "accepted route xclbin target is not hw",
771 ));
772 }
773 if !is_lower_hex(&xclbin.descriptor_identity_sha256, 64, true)
774 || xclbin.descriptor_identity_sha256
775 != xclbin_identity_sha256(
776 &xclbin.subject,
777 &xclbin.uuid,
778 &xclbin.interface_uuid,
779 &xclbin.deployment_platform,
780 &xclbin.fpga_part,
781 &xclbin.target,
782 )?
783 {
784 return Err(PromotionError::new(
785 FailureCode::IdentityJoin,
786 "route-derived xclbin identity is not bound to its exact descriptor subject",
787 ));
788 }
789 Ok(())
790}
791
792fn validate_route_status(status: &RouteStatusObservation) -> PromotionResult<()> {
793 if !status.placed_fully
794 || !status.routed_fully
795 || status.partially_routed
796 || status.route_errors
797 {
798 return Err(PromotionError::new(
799 FailureCode::RouteTiming,
800 "accepted route must be completely placed and routed with no route errors",
801 ));
802 }
803 Ok(())
804}
805
806fn validate_timing(timing: &RouteTimingObservation) -> PromotionResult<()> {
807 if !timing.complete_timing_coverage
808 || timing.setup_path_count == 0
809 || timing.hold_path_count == 0
810 || timing.unconstrained_path_count != 0
811 || timing.unconstrained_endpoint_count != 0
812 || timing.setup_wns_femtoseconds < 0
813 || timing.hold_whs_femtoseconds < 0
814 {
815 return Err(PromotionError::new(
816 FailureCode::RouteTiming,
817 "accepted route lacks complete constrained setup/hold coverage or has negative slack",
818 ));
819 }
820 Ok(())
821}
822
823fn validate_drc(drc: &RouteDrcObservation) -> PromotionResult<()> {
824 if !drc.complete_no_waivers_evaluation
825 || drc.enabled_error_violations != 0
826 || drc.enabled_fatal_violations != 0
827 || drc.installed_waivers != 0
828 {
829 return Err(PromotionError::new(
830 FailureCode::RouteTiming,
831 "accepted route lacks a complete no-waivers DRC pass",
832 ));
833 }
834 Ok(())
835}
836
837fn validate_resources(resources: &RouteResourceObservation) -> PromotionResult<()> {
838 let fixed_caps = resources.declared_lut_cap == MAXIMUM_LUTS
839 && resources.declared_register_cap == MAXIMUM_REGISTERS
840 && resources.declared_dsp_cap == MAXIMUM_DSPS;
841 let within_caps = resources.luts <= MAXIMUM_LUTS
842 && resources.registers <= MAXIMUM_REGISTERS
843 && resources.dsps <= MAXIMUM_DSPS;
844 if resources.scope != ResourceScope::WholeRoutedDesignIncludingShell
845 || !fixed_caps
846 || !within_caps
847 {
848 return Err(PromotionError::new(
849 FailureCode::RouteResource,
850 "route resources must cover the whole routed design including shell and satisfy the fixed 200k LUT/300k register/4k DSP policy",
851 ));
852 }
853 Ok(())
854}
855
856fn validate_nonclaims(nonclaims: &RouteNonclaims) -> PromotionResult<()> {
857 if nonclaims.hardware_execution
858 || nonclaims.card_validated
859 || nonclaims.cryptographic_outputs_valid
860 || nonclaims.performance_validated
861 || nonclaims.hardware_completion_promotable
862 {
863 return Err(PromotionError::new(
864 FailureCode::PrematureClaim,
865 "route-tier input contains a premature hardware, card, cryptographic, performance, or completion claim",
866 ));
867 }
868 Ok(())
869}
870
871fn require_strict_child(root: &str, path: &str, label: &str) -> PromotionResult<()> {
872 let prefix = format!("{root}/");
873 if !path.starts_with(&prefix) {
874 return Err(PromotionError::new(
875 FailureCode::IdentityJoin,
876 format!("{label} is not beneath the exact acceptance-run root"),
877 ));
878 }
879 Ok(())
880}
881
882fn require_same_artifact(
883 expected: &RouteArtifactBinding,
884 observed: &RouteArtifactBinding,
885 label: &str,
886) -> PromotionResult<()> {
887 if expected != observed {
888 return Err(PromotionError::new(
889 FailureCode::IdentityJoin,
890 format!("{label} does not repeat one exact artifact identity"),
891 ));
892 }
893 Ok(())
894}
895
896fn validate_raw_report_bindings(
897 acceptance_root: &str,
898 inventory_reports: &[RouteArtifactBinding],
899 seal_reports: &[RouteArtifactBinding],
900) -> PromotionResult<()> {
901 validate_raw_report_set(
902 acceptance_root,
903 inventory_reports,
904 "run-inventory raw reports",
905 )?;
906 validate_raw_report_set(acceptance_root, seal_reports, "route-seal raw reports")?;
907 if inventory_reports != seal_reports {
908 return Err(PromotionError::new(
909 FailureCode::IdentityJoin,
910 "route seal does not bind the exact run-inventory raw-report set",
911 ));
912 }
913 Ok(())
914}
915
916fn validate_raw_report_set(
917 acceptance_root: &str,
918 reports: &[RouteArtifactBinding],
919 label: &str,
920) -> PromotionResult<()> {
921 if reports.is_empty() || reports.len() > MAXIMUM_RAW_ROUTE_REPORTS {
922 return Err(PromotionError::new(
923 FailureCode::RouteUnaccepted,
924 format!("{label} must contain 1..={MAXIMUM_RAW_ROUTE_REPORTS} exact artifacts"),
925 ));
926 }
927 let mut previous_path: Option<&str> = None;
928 for (index, report) in reports.iter().enumerate() {
929 report.validate(&format!("{label}[{index}]"))?;
930 require_strict_child(
931 acceptance_root,
932 report.absolute_path(),
933 &format!("{label}[{index}]"),
934 )?;
935 if let Some(previous) = previous_path {
936 if previous == report.absolute_path() {
937 return Err(PromotionError::new(
938 FailureCode::IdentityJoin,
939 format!("{label} aliases a raw-report path"),
940 ));
941 }
942 if previous > report.absolute_path() {
943 return Err(PromotionError::new(
944 FailureCode::Noncanonical,
945 format!("{label} is not strictly sorted by canonical absolute path"),
946 ));
947 }
948 }
949 previous_path = Some(report.absolute_path());
950 }
951 Ok(())
952}
953
954fn validate_observation_provenance(
955 provenance: &RouteObservationProvenance,
956 run_inventory: &RouteArtifactBinding,
957 route_seal: &RouteArtifactBinding,
958 raw_reports: &[RouteArtifactBinding],
959 raw_report_set_sha256: &str,
960 label: &str,
961) -> PromotionResult<()> {
962 provenance
963 .run_inventory
964 .validate(&format!("{label} run-inventory subject"))?;
965 provenance
966 .route_seal
967 .validate(&format!("{label} route-seal subject"))?;
968 provenance
969 .source_report
970 .validate(&format!("{label} source-report subject"))?;
971 if !is_lower_hex(&provenance.raw_report_set_sha256, 64, true) {
972 return Err(PromotionError::new(
973 FailureCode::Noncanonical,
974 format!("{label} has a non-canonical raw-report closure digest"),
975 ));
976 }
977 if &provenance.run_inventory != run_inventory
978 || &provenance.route_seal != route_seal
979 || provenance.raw_report_set_sha256 != raw_report_set_sha256
980 || !raw_reports
981 .iter()
982 .any(|report| report == &provenance.source_report)
983 {
984 return Err(PromotionError::new(
985 FailureCode::IdentityJoin,
986 format!(
987 "{label} is not bound to the exact inventory, seal, raw-report closure, and source report"
988 ),
989 ));
990 }
991 Ok(())
992}
993
994fn canonical_raw_report_set_sha256(reports: &[RouteArtifactBinding]) -> PromotionResult<String> {
995 let mut hasher = Sha256::new();
996 hasher.update(REPORT_SET_DIGEST_DOMAIN);
997 let report_count = u64::try_from(reports.len()).map_err(|_| {
998 PromotionError::new(
999 FailureCode::RouteUnaccepted,
1000 "raw-report set exceeds the canonical digest count boundary",
1001 )
1002 })?;
1003 hasher.update(report_count.to_be_bytes());
1004 for report in reports {
1005 update_artifact_subject_digest(&mut hasher, report)?;
1006 }
1007 Ok(hex_lower(&hasher.finalize()))
1008}
1009
1010fn xclbin_identity_sha256(
1011 subject: &RouteArtifactBinding,
1012 uuid: &str,
1013 interface_uuid: &str,
1014 deployment_platform: &str,
1015 fpga_part: &str,
1016 target: &str,
1017) -> PromotionResult<String> {
1018 let mut hasher = Sha256::new();
1019 hasher.update(XCLBIN_IDENTITY_DIGEST_DOMAIN);
1020 update_artifact_subject_digest(&mut hasher, subject)?;
1021 for value in [uuid, interface_uuid, deployment_platform, fpga_part, target] {
1022 update_length_prefixed(&mut hasher, value.as_bytes())?;
1023 }
1024 Ok(hex_lower(&hasher.finalize()))
1025}
1026
1027fn update_artifact_subject_digest(
1028 hasher: &mut Sha256,
1029 artifact: &RouteArtifactBinding,
1030) -> PromotionResult<()> {
1031 update_length_prefixed(hasher, artifact.absolute_path.as_bytes())?;
1032 update_length_prefixed(hasher, artifact.descriptor.canonical_realpath.as_bytes())?;
1033 hasher.update(artifact.bytes.to_be_bytes());
1034 update_length_prefixed(hasher, artifact.sha256.as_bytes())?;
1035 hasher.update(artifact.descriptor.device.to_be_bytes());
1036 hasher.update(artifact.descriptor.inode.to_be_bytes());
1037 Ok(())
1038}
1039
1040fn update_length_prefixed(hasher: &mut Sha256, value: &[u8]) -> PromotionResult<()> {
1041 let length = u64::try_from(value.len()).map_err(|_| {
1042 PromotionError::new(
1043 FailureCode::RouteUnaccepted,
1044 "route evidence subject exceeds the canonical digest length boundary",
1045 )
1046 })?;
1047 hasher.update(length.to_be_bytes());
1048 hasher.update(value);
1049 Ok(())
1050}
1051
1052fn require_unique_artifact_subject<'a>(
1053 paths: &mut BTreeSet<&'a str>,
1054 physical_subjects: &mut BTreeSet<(u64, u64)>,
1055 artifact: &'a RouteArtifactBinding,
1056) -> PromotionResult<()> {
1057 if !paths.insert(artifact.absolute_path()) {
1058 return Err(PromotionError::new(
1059 FailureCode::IdentityJoin,
1060 "accepted route proof lexically aliases independently bound artifacts",
1061 ));
1062 }
1063 if !physical_subjects.insert((artifact.descriptor.device, artifact.descriptor.inode)) {
1064 return Err(PromotionError::new(
1065 FailureCode::IdentityJoin,
1066 "accepted route proof aliases descriptor subjects by device and inode",
1067 ));
1068 }
1069 Ok(())
1070}
1071
1072fn is_canonical_uuid(value: &str) -> bool {
1073 let bytes = value.as_bytes();
1074 bytes.len() == 36
1075 && bytes.iter().enumerate().all(|(index, byte)| {
1076 if matches!(index, 8 | 13 | 18 | 23) {
1077 *byte == b'-'
1078 } else {
1079 byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)
1080 }
1081 })
1082 && bytes
1083 .iter()
1084 .enumerate()
1085 .any(|(index, byte)| !matches!(index, 8 | 13 | 18 | 23) && *byte != b'0')
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091 use crate::model::synthetic_request;
1092
1093 const XCLBIN_UUID: &str = "11111111-2222-3333-4444-555555555555";
1094 const INTERFACE_UUID: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
1095
1096 fn subject(
1097 absolute_path: &str,
1098 bytes: u64,
1099 sha256: String,
1100 inode: u64,
1101 ) -> RouteArtifactBinding {
1102 RouteArtifactBinding::from_exact_parser(
1103 absolute_path.to_owned(),
1104 absolute_path.to_owned(),
1105 bytes,
1106 sha256,
1107 7,
1108 inode,
1109 )
1110 }
1111
1112 fn artifact(locator: &RequestFileLocator<'_>, inode: u64) -> RouteArtifactBinding {
1113 subject(
1114 locator.absolute_path(),
1115 locator.bytes(),
1116 locator.sha256().to_owned(),
1117 inode,
1118 )
1119 }
1120
1121 fn repeat_artifact(artifact: &RouteArtifactBinding) -> RouteArtifactBinding {
1122 RouteArtifactBinding::from_exact_parser(
1123 artifact.absolute_path.clone(),
1124 artifact.descriptor.canonical_realpath.clone(),
1125 artifact.bytes,
1126 artifact.sha256.clone(),
1127 artifact.descriptor.device,
1128 artifact.descriptor.inode,
1129 )
1130 }
1131
1132 fn clock_artifact() -> RouteArtifactBinding {
1133 subject(
1134 "/evidence/route-acceptance/evidence/post-route-evidence.json",
1135 100,
1136 "b".repeat(64),
1137 30,
1138 )
1139 }
1140
1141 fn xclbin_artifact() -> RouteArtifactBinding {
1142 subject(
1143 "/evidence/route-acceptance/output/kernel.xclbin",
1144 1_000,
1145 "c".repeat(64),
1146 40,
1147 )
1148 }
1149
1150 fn raw_report_artifacts() -> Vec<RouteArtifactBinding> {
1151 vec![
1152 subject(
1153 "/evidence/route-acceptance/reports/report-a.raw",
1154 200,
1155 "d".repeat(64),
1156 20,
1157 ),
1158 subject(
1159 "/evidence/route-acceptance/reports/report-b.raw",
1160 300,
1161 "e".repeat(64),
1162 21,
1163 ),
1164 ]
1165 }
1166
1167 fn xclbin_identity() -> RouteXclbinIdentity {
1168 RouteXclbinIdentity::from_exact_parser(
1169 xclbin_artifact(),
1170 XCLBIN_UUID.to_owned(),
1171 INTERFACE_UUID.to_owned(),
1172 REQUIRED_PLATFORM.to_owned(),
1173 REQUIRED_DEVICE_PART.to_owned(),
1174 REQUIRED_XCLBIN_TARGET.to_owned(),
1175 )
1176 .unwrap()
1177 }
1178
1179 fn provenance(
1180 run_inventory: &RouteArtifactBinding,
1181 route_seal: &RouteArtifactBinding,
1182 raw_reports: &[RouteArtifactBinding],
1183 source_index: usize,
1184 ) -> RouteObservationProvenance {
1185 RouteObservationProvenance {
1186 run_inventory: repeat_artifact(run_inventory),
1187 route_seal: repeat_artifact(route_seal),
1188 raw_report_set_sha256: canonical_raw_report_set_sha256(raw_reports).unwrap(),
1189 source_report: repeat_artifact(&raw_reports[source_index]),
1190 }
1191 }
1192
1193 #[allow(clippy::too_many_lines)] fn fixture(request: &PromotionRequest) -> RoutePolicyInputs {
1195 let locators = request.route_locators();
1196 let stage_manifest = artifact(locators.staged_run_manifest(), 1);
1197 let run_inventory = artifact(locators.complete_run_inventory(), 2);
1198 let route_seal = artifact(locators.accepted_250mhz_route_seal(), 3);
1199 let inventory_raw_reports = raw_report_artifacts();
1200 let seal_raw_reports = raw_report_artifacts();
1201 let routed_clock_evidence = clock_artifact();
1202 let xclbin_identity = xclbin_identity();
1203 let inventory_routed_clock_evidence = repeat_artifact(&routed_clock_evidence);
1204 let seal_routed_clock_evidence = repeat_artifact(&routed_clock_evidence);
1205 let inventory_xclbin = repeat_artifact(xclbin_identity.subject());
1206 let seal_xclbin = repeat_artifact(xclbin_identity.subject());
1207 let seal_run_inventory = repeat_artifact(&run_inventory);
1208 RoutePolicyInputs {
1209 identity: BoundRouteObservation {
1210 provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 0),
1211 value: RouteIdentityObservation {
1212 evidence_tier: ACCEPTANCE_EVIDENCE_TIER.to_owned(),
1213 capture_kind: ACCEPTANCE_CAPTURE_KIND.to_owned(),
1214 status: ACCEPTANCE_STATUS.to_owned(),
1215 profile: PERFORMANCE_PROFILE.to_owned(),
1216 amd_release: REQUIRED_AMD_RELEASE.to_owned(),
1217 deployment_platform: REQUIRED_PLATFORM.to_owned(),
1218 device_part: REQUIRED_DEVICE_PART.to_owned(),
1219 route_design_digest: "a".repeat(64),
1220 requested_clock_hz: BASELINE_CLOCK_HZ,
1221 actual_routed_hz: BASELINE_CLOCK_HZ,
1222 },
1223 },
1224 stage_identity: StageManifestIdentityObservation {
1225 source: repeat_artifact(&stage_manifest),
1226 route_provenance: provenance(
1227 &run_inventory,
1228 &route_seal,
1229 &inventory_raw_reports,
1230 0,
1231 ),
1232 stage_subject_sha256: locators.staged_run_manifest().sha256().to_owned(),
1233 design_digest: "a".repeat(64),
1234 },
1235 inventory_identity: RunInventoryIdentityObservation {
1236 source: repeat_artifact(&run_inventory),
1237 route_provenance: provenance(
1238 &run_inventory,
1239 &route_seal,
1240 &inventory_raw_reports,
1241 0,
1242 ),
1243 stage_subject_sha256: locators.staged_run_manifest().sha256().to_owned(),
1244 design_digest: "a".repeat(64),
1245 },
1246 status: BoundRouteObservation {
1247 provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 0),
1248 value: RouteStatusObservation {
1249 placed_fully: true,
1250 routed_fully: true,
1251 partially_routed: false,
1252 route_errors: false,
1253 },
1254 },
1255 timing: BoundRouteObservation {
1256 provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 0),
1257 value: RouteTimingObservation {
1258 setup_wns_femtoseconds: 0,
1259 hold_whs_femtoseconds: 0,
1260 setup_path_count: 1,
1261 hold_path_count: 1,
1262 unconstrained_path_count: 0,
1263 unconstrained_endpoint_count: 0,
1264 complete_timing_coverage: true,
1265 },
1266 },
1267 drc: BoundRouteObservation {
1268 provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 1),
1269 value: RouteDrcObservation {
1270 complete_no_waivers_evaluation: true,
1271 enabled_error_violations: 0,
1272 enabled_fatal_violations: 0,
1273 installed_waivers: 0,
1274 },
1275 },
1276 resources: BoundRouteObservation {
1277 provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 1),
1278 value: RouteResourceObservation {
1279 scope: ResourceScope::WholeRoutedDesignIncludingShell,
1280 luts: MAXIMUM_LUTS,
1281 registers: MAXIMUM_REGISTERS,
1282 dsps: MAXIMUM_DSPS,
1283 declared_lut_cap: MAXIMUM_LUTS,
1284 declared_register_cap: MAXIMUM_REGISTERS,
1285 declared_dsp_cap: MAXIMUM_DSPS,
1286 },
1287 },
1288 nonclaims: RouteNonclaims {
1289 hardware_execution: false,
1290 card_validated: false,
1291 cryptographic_outputs_valid: false,
1292 performance_validated: false,
1293 hardware_completion_promotable: false,
1294 },
1295 stage_manifest,
1296 run_inventory,
1297 route_seal,
1298 inventory_raw_reports,
1299 seal_raw_reports,
1300 routed_clock_evidence,
1301 xclbin_identity,
1302 inventory_routed_clock_evidence,
1303 inventory_xclbin,
1304 seal_run_inventory,
1305 seal_routed_clock_evidence,
1306 seal_xclbin,
1307 }
1308 }
1309
1310 #[allow(clippy::large_types_passed_by_value)] fn assert_rejected(input: RoutePolicyInputs, code: FailureCode) {
1312 let request = synthetic_request(&"1".repeat(32));
1313 assert_eq!(
1314 validate_route_policy_inputs(&request, input)
1315 .expect_err("mutated route proof must fail closed")
1316 .code(),
1317 code
1318 );
1319 }
1320
1321 #[test]
1322 fn exact_policy_boundary_produces_metadata_not_accepted_evidence() {
1323 let request = synthetic_request(&"1".repeat(32));
1324 let policy = validate_route_policy_inputs(&request, fixture(&request)).unwrap();
1325 assert_eq!(policy.design_digest, "a".repeat(64));
1326 assert_eq!(policy.actual_routed_hz, BASELINE_CLOCK_HZ);
1327 assert_eq!(policy.setup_wns_femtoseconds, 0);
1328 assert_eq!(policy.hold_whs_femtoseconds, 0);
1329 assert_eq!(policy.whole_design_luts, MAXIMUM_LUTS);
1330 assert_eq!(policy.whole_design_registers, MAXIMUM_REGISTERS);
1331 assert_eq!(policy.whole_design_dsps, MAXIMUM_DSPS);
1332 assert_eq!(policy.raw_reports.len(), 2);
1333 assert_eq!(policy.raw_reports[0].sha256(), "d".repeat(64));
1334 assert_eq!(policy.xclbin_identity.subject().bytes(), 1_000);
1335 assert_eq!(policy.xclbin_identity.subject().sha256(), "c".repeat(64));
1336 assert_eq!(
1337 policy.xclbin_identity.subject().canonical_realpath(),
1338 "/evidence/route-acceptance/output/kernel.xclbin"
1339 );
1340 assert_eq!(policy.xclbin_identity.subject().device(), 7);
1341 assert_eq!(policy.xclbin_identity.subject().inode(), 40);
1342 assert_eq!(policy.xclbin_identity.uuid(), XCLBIN_UUID);
1343 assert_eq!(policy.xclbin_identity.interface_uuid(), INTERFACE_UUID);
1344 assert_eq!(
1345 policy.xclbin_identity.deployment_platform(),
1346 REQUIRED_PLATFORM
1347 );
1348 assert_eq!(policy.xclbin_identity.fpga_part(), REQUIRED_DEVICE_PART);
1349 assert_eq!(policy.xclbin_identity.target(), REQUIRED_XCLBIN_TARGET);
1350 }
1351
1352 #[test]
1353 fn routed_clock_above_the_baseline_remains_acceptable() {
1354 let request = synthetic_request(&"1".repeat(32));
1355 let mut input = fixture(&request);
1356 input.identity.value.actual_routed_hz = BASELINE_CLOCK_HZ + 1;
1357 let policy = validate_route_policy_inputs(&request, input).unwrap();
1358 assert_eq!(policy.actual_routed_hz, BASELINE_CLOCK_HZ + 1);
1359 }
1360
1361 #[test]
1362 fn public_route_entry_remains_unreachable() {
1363 let request = synthetic_request(&"1".repeat(32));
1364 assert_eq!(
1365 require_accepted_route(&request).unwrap_err().code(),
1366 FailureCode::RouteUnaccepted
1367 );
1368 }
1369
1370 #[test]
1371 fn identity_mutations_fail_before_route_acceptance() {
1372 let request = synthetic_request(&"1".repeat(32));
1373
1374 let mut wrong_tier = fixture(&request);
1375 wrong_tier.identity.value.evidence_tier = "format-probe-capture".to_owned();
1376 assert_rejected(wrong_tier, FailureCode::RouteUnaccepted);
1377
1378 let mut wrong_capture = fixture(&request);
1379 wrong_capture.identity.value.capture_kind = "format-probe-capture".to_owned();
1380 assert_rejected(wrong_capture, FailureCode::RouteUnaccepted);
1381
1382 let mut failed_status = fixture(&request);
1383 failed_status.identity.value.status = "failed".to_owned();
1384 assert_rejected(failed_status, FailureCode::RouteUnaccepted);
1385
1386 let mut wrong_profile = fixture(&request);
1387 wrong_profile.identity.value.profile = "LEGACY_KECCAK".to_owned();
1388 assert_rejected(wrong_profile, FailureCode::RouteUnaccepted);
1389
1390 let mut wrong_release = fixture(&request);
1391 wrong_release.identity.value.amd_release = "2024.1".to_owned();
1392 assert_rejected(wrong_release, FailureCode::RouteUnaccepted);
1393
1394 let mut wrong_platform = fixture(&request);
1395 wrong_platform.identity.value.deployment_platform = "other-platform".to_owned();
1396 assert_rejected(wrong_platform, FailureCode::RouteUnaccepted);
1397
1398 let mut wrong_part = fixture(&request);
1399 wrong_part.identity.value.device_part = "other-part".to_owned();
1400 assert_rejected(wrong_part, FailureCode::RouteUnaccepted);
1401
1402 let mut zero_design = fixture(&request);
1403 zero_design.identity.value.route_design_digest = "0".repeat(64);
1404 assert_rejected(zero_design, FailureCode::RouteUnaccepted);
1405
1406 let mut mismatched_design = fixture(&request);
1407 mismatched_design.identity.value.route_design_digest = "d".repeat(64);
1408 assert_rejected(mismatched_design, FailureCode::IdentityJoin);
1409
1410 let mut changed_stage_subject = fixture(&request);
1411 changed_stage_subject.stage_identity.stage_subject_sha256 = "d".repeat(64);
1412 assert_rejected(changed_stage_subject, FailureCode::IdentityJoin);
1413
1414 let mut changed_inventory_subject = fixture(&request);
1415 changed_inventory_subject
1416 .inventory_identity
1417 .stage_subject_sha256 = "d".repeat(64);
1418 assert_rejected(changed_inventory_subject, FailureCode::IdentityJoin);
1419
1420 let mut changed_stage_design = fixture(&request);
1421 changed_stage_design.stage_identity.design_digest = "d".repeat(64);
1422 assert_rejected(changed_stage_design, FailureCode::IdentityJoin);
1423
1424 let mut changed_inventory_design = fixture(&request);
1425 changed_inventory_design.inventory_identity.design_digest = "d".repeat(64);
1426 assert_rejected(changed_inventory_design, FailureCode::IdentityJoin);
1427
1428 let mut noncanonical_stage_subject = fixture(&request);
1429 noncanonical_stage_subject
1430 .stage_identity
1431 .stage_subject_sha256 = "0".repeat(64);
1432 assert_rejected(noncanonical_stage_subject, FailureCode::Noncanonical);
1433
1434 let mut probe_clock = fixture(&request);
1435 probe_clock.identity.value.requested_clock_hz = 300_000_000;
1436 assert_rejected(probe_clock, FailureCode::RouteUnaccepted);
1437
1438 let mut slow_route = fixture(&request);
1439 slow_route.identity.value.actual_routed_hz = BASELINE_CLOCK_HZ - 1;
1440 assert_rejected(slow_route, FailureCode::RouteTiming);
1441 }
1442
1443 #[test]
1444 fn every_parsed_observation_rejects_subject_or_closure_splicing() {
1445 let request = synthetic_request(&"1".repeat(32));
1446
1447 let mut identity_inventory_splice = fixture(&request);
1448 identity_inventory_splice
1449 .identity
1450 .provenance
1451 .run_inventory
1452 .sha256 = "f".repeat(64);
1453 assert_rejected(identity_inventory_splice, FailureCode::IdentityJoin);
1454
1455 let mut status_source_splice = fixture(&request);
1456 status_source_splice.status.provenance.source_report = xclbin_artifact();
1457 assert_rejected(status_source_splice, FailureCode::IdentityJoin);
1458
1459 let mut timing_seal_splice = fixture(&request);
1460 timing_seal_splice.timing.provenance.route_seal.bytes += 1;
1461 assert_rejected(timing_seal_splice, FailureCode::IdentityJoin);
1462
1463 let mut timing_same_bytes_other_route = fixture(&request);
1464 timing_same_bytes_other_route
1465 .timing
1466 .provenance
1467 .source_report
1468 .descriptor
1469 .inode += 100;
1470 assert_rejected(timing_same_bytes_other_route, FailureCode::IdentityJoin);
1471
1472 let mut drc_closure_splice = fixture(&request);
1473 drc_closure_splice.drc.provenance.raw_report_set_sha256 = "f".repeat(64);
1474 assert_rejected(drc_closure_splice, FailureCode::IdentityJoin);
1475
1476 let mut resource_source_splice = fixture(&request);
1477 resource_source_splice.resources.provenance.source_report = subject(
1478 "/evidence/route-acceptance/reports/other-report.raw",
1479 400,
1480 "f".repeat(64),
1481 22,
1482 );
1483 assert_rejected(resource_source_splice, FailureCode::IdentityJoin);
1484
1485 let mut stage_source_splice = fixture(&request);
1486 let inventory_subject = repeat_artifact(&stage_source_splice.run_inventory);
1487 stage_source_splice.stage_identity.source = inventory_subject;
1488 assert_rejected(stage_source_splice, FailureCode::IdentityJoin);
1489
1490 let mut stage_closure_splice = fixture(&request);
1491 stage_closure_splice
1492 .stage_identity
1493 .route_provenance
1494 .raw_report_set_sha256 = "f".repeat(64);
1495 assert_rejected(stage_closure_splice, FailureCode::IdentityJoin);
1496
1497 let mut inventory_source_splice = fixture(&request);
1498 let stage_subject = repeat_artifact(&inventory_source_splice.stage_manifest);
1499 inventory_source_splice.inventory_identity.source = stage_subject;
1500 assert_rejected(inventory_source_splice, FailureCode::IdentityJoin);
1501 }
1502
1503 #[test]
1504 fn incomplete_route_timing_and_drc_fail_closed() {
1505 let request = synthetic_request(&"1".repeat(32));
1506
1507 let mut partial = fixture(&request);
1508 partial.status.value.partially_routed = true;
1509 assert_rejected(partial, FailureCode::RouteTiming);
1510
1511 let mut unplaced = fixture(&request);
1512 unplaced.status.value.placed_fully = false;
1513 assert_rejected(unplaced, FailureCode::RouteTiming);
1514
1515 let mut unrouted = fixture(&request);
1516 unrouted.status.value.routed_fully = false;
1517 assert_rejected(unrouted, FailureCode::RouteTiming);
1518
1519 let mut route_error = fixture(&request);
1520 route_error.status.value.route_errors = true;
1521 assert_rejected(route_error, FailureCode::RouteTiming);
1522
1523 let mut negative_setup = fixture(&request);
1524 negative_setup.timing.value.setup_wns_femtoseconds = -1;
1525 assert_rejected(negative_setup, FailureCode::RouteTiming);
1526
1527 let mut negative_hold = fixture(&request);
1528 negative_hold.timing.value.hold_whs_femtoseconds = -1;
1529 assert_rejected(negative_hold, FailureCode::RouteTiming);
1530
1531 let mut incomplete_coverage = fixture(&request);
1532 incomplete_coverage.timing.value.complete_timing_coverage = false;
1533 assert_rejected(incomplete_coverage, FailureCode::RouteTiming);
1534
1535 let mut missing_hold = fixture(&request);
1536 missing_hold.timing.value.hold_path_count = 0;
1537 assert_rejected(missing_hold, FailureCode::RouteTiming);
1538
1539 let mut missing_setup = fixture(&request);
1540 missing_setup.timing.value.setup_path_count = 0;
1541 assert_rejected(missing_setup, FailureCode::RouteTiming);
1542
1543 let mut unconstrained = fixture(&request);
1544 unconstrained.timing.value.unconstrained_endpoint_count = 1;
1545 assert_rejected(unconstrained, FailureCode::RouteTiming);
1546
1547 let mut unconstrained_path = fixture(&request);
1548 unconstrained_path.timing.value.unconstrained_path_count = 1;
1549 assert_rejected(unconstrained_path, FailureCode::RouteTiming);
1550
1551 let mut incomplete_drc = fixture(&request);
1552 incomplete_drc.drc.value.complete_no_waivers_evaluation = false;
1553 assert_rejected(incomplete_drc, FailureCode::RouteTiming);
1554
1555 let mut waived = fixture(&request);
1556 waived.drc.value.installed_waivers = 1;
1557 assert_rejected(waived, FailureCode::RouteTiming);
1558
1559 let mut error = fixture(&request);
1560 error.drc.value.enabled_error_violations = 1;
1561 assert_rejected(error, FailureCode::RouteTiming);
1562
1563 let mut fatal = fixture(&request);
1564 fatal.drc.value.enabled_fatal_violations = 1;
1565 assert_rejected(fatal, FailureCode::RouteTiming);
1566 }
1567
1568 #[test]
1569 fn kernel_only_relaxed_or_over_limit_resources_fail_closed() {
1570 let request = synthetic_request(&"1".repeat(32));
1571
1572 let mut kernel_only = fixture(&request);
1573 kernel_only.resources.value.scope = ResourceScope::KernelHierarchyOnly;
1574 assert_rejected(kernel_only, FailureCode::RouteResource);
1575
1576 let mut relaxed_cap = fixture(&request);
1577 relaxed_cap.resources.value.declared_dsp_cap = MAXIMUM_DSPS + 1;
1578 assert_rejected(relaxed_cap, FailureCode::RouteResource);
1579
1580 let mut relaxed_lut_cap = fixture(&request);
1581 relaxed_lut_cap.resources.value.declared_lut_cap = MAXIMUM_LUTS + 1;
1582 assert_rejected(relaxed_lut_cap, FailureCode::RouteResource);
1583
1584 let mut relaxed_register_cap = fixture(&request);
1585 relaxed_register_cap.resources.value.declared_register_cap = MAXIMUM_REGISTERS + 1;
1586 assert_rejected(relaxed_register_cap, FailureCode::RouteResource);
1587
1588 let mut excess_lut = fixture(&request);
1589 excess_lut.resources.value.luts = MAXIMUM_LUTS + 1;
1590 assert_rejected(excess_lut, FailureCode::RouteResource);
1591
1592 let mut excess_register = fixture(&request);
1593 excess_register.resources.value.registers = MAXIMUM_REGISTERS + 1;
1594 assert_rejected(excess_register, FailureCode::RouteResource);
1595
1596 let mut excess_dsp = fixture(&request);
1597 excess_dsp.resources.value.dsps = MAXIMUM_DSPS + 1;
1598 assert_rejected(excess_dsp, FailureCode::RouteResource);
1599 }
1600
1601 #[test]
1602 fn artifact_splices_aliases_and_noncanonical_bindings_fail_closed() {
1603 let request = synthetic_request(&"1".repeat(32));
1604
1605 let mut changed_inventory = fixture(&request);
1606 changed_inventory.run_inventory.sha256 = "d".repeat(64);
1607 assert_rejected(changed_inventory, FailureCode::IdentityJoin);
1608
1609 let mut changed_stage = fixture(&request);
1610 changed_stage.stage_manifest.bytes += 1;
1611 assert_rejected(changed_stage, FailureCode::IdentityJoin);
1612
1613 let mut changed_seal = fixture(&request);
1614 changed_seal.route_seal.sha256 = "e".repeat(64);
1615 assert_rejected(changed_seal, FailureCode::IdentityJoin);
1616
1617 let mut inventory_xclbin_splice = fixture(&request);
1618 inventory_xclbin_splice.inventory_xclbin.sha256 = "d".repeat(64);
1619 assert_rejected(inventory_xclbin_splice, FailureCode::IdentityJoin);
1620
1621 let mut inventory_clock_splice = fixture(&request);
1622 inventory_clock_splice
1623 .inventory_routed_clock_evidence
1624 .sha256 = "d".repeat(64);
1625 assert_rejected(inventory_clock_splice, FailureCode::IdentityJoin);
1626
1627 let mut seal_clock_splice = fixture(&request);
1628 seal_clock_splice.seal_routed_clock_evidence.bytes += 1;
1629 assert_rejected(seal_clock_splice, FailureCode::IdentityJoin);
1630
1631 let mut seal_inventory_splice = fixture(&request);
1632 seal_inventory_splice.seal_run_inventory.sha256 = "e".repeat(64);
1633 assert_rejected(seal_inventory_splice, FailureCode::IdentityJoin);
1634
1635 let mut seal_xclbin_splice = fixture(&request);
1636 seal_xclbin_splice.seal_xclbin.bytes += 1;
1637 assert_rejected(seal_xclbin_splice, FailureCode::IdentityJoin);
1638
1639 let mut outside_root = fixture(&request);
1640 outside_root.xclbin_identity.subject.absolute_path = "/other/kernel.xclbin".to_owned();
1641 assert_rejected(outside_root, FailureCode::IdentityJoin);
1642
1643 let mut clock_outside_root = fixture(&request);
1644 clock_outside_root.routed_clock_evidence.absolute_path =
1645 "/other/post-route-evidence.json".to_owned();
1646 assert_rejected(clock_outside_root, FailureCode::IdentityJoin);
1647
1648 let mut alias = fixture(&request);
1649 alias.routed_clock_evidence = xclbin_artifact();
1650 alias.inventory_routed_clock_evidence = xclbin_artifact();
1651 alias.seal_routed_clock_evidence = xclbin_artifact();
1652 assert_rejected(alias, FailureCode::IdentityJoin);
1653
1654 let mut zero_length = fixture(&request);
1655 zero_length.xclbin_identity.subject.bytes = 0;
1656 assert_rejected(zero_length, FailureCode::Noncanonical);
1657
1658 let mut uppercase_digest = fixture(&request);
1659 uppercase_digest.xclbin_identity.subject.sha256 = "A".repeat(64);
1660 assert_rejected(uppercase_digest, FailureCode::Noncanonical);
1661 }
1662
1663 #[test]
1664 fn parser_derived_symlink_realpath_and_hardlink_aliases_fail_closed() {
1665 let request = synthetic_request(&"1".repeat(32));
1666
1667 let mut realpath_alias = fixture(&request);
1669 realpath_alias
1670 .routed_clock_evidence
1671 .descriptor
1672 .canonical_realpath = "/evidence/route-acceptance/evidence/other-clock.json".to_owned();
1673 assert_rejected(realpath_alias, FailureCode::IdentityJoin);
1674
1675 let mut hardlink_alias = fixture(&request);
1676 let xclbin_device = hardlink_alias.xclbin_identity.subject.descriptor.device;
1677 let xclbin_inode = hardlink_alias.xclbin_identity.subject.descriptor.inode;
1678 hardlink_alias.inventory_raw_reports[0].descriptor.device = xclbin_device;
1679 hardlink_alias.inventory_raw_reports[0].descriptor.inode = xclbin_inode;
1680 hardlink_alias.seal_raw_reports[0].descriptor.device = xclbin_device;
1681 hardlink_alias.seal_raw_reports[0].descriptor.inode = xclbin_inode;
1682 assert_rejected(hardlink_alias, FailureCode::IdentityJoin);
1683
1684 let mut missing_inode = fixture(&request);
1685 missing_inode.route_seal.descriptor.inode = 0;
1686 assert_rejected(missing_inode, FailureCode::Noncanonical);
1687 }
1688
1689 #[test]
1690 fn raw_report_omissions_splices_ordering_and_aliases_fail_closed() {
1691 let request = synthetic_request(&"1".repeat(32));
1692
1693 let mut empty = fixture(&request);
1694 empty.inventory_raw_reports.clear();
1695 empty.seal_raw_reports.clear();
1696 assert_rejected(empty, FailureCode::RouteUnaccepted);
1697
1698 let mut unsorted = fixture(&request);
1699 unsorted.inventory_raw_reports.swap(0, 1);
1700 unsorted.seal_raw_reports.swap(0, 1);
1701 assert_rejected(unsorted, FailureCode::Noncanonical);
1702
1703 let mut duplicate = fixture(&request);
1704 let duplicate_path = duplicate.inventory_raw_reports[0].absolute_path.clone();
1705 duplicate.inventory_raw_reports[1].absolute_path = duplicate_path.clone();
1706 duplicate.inventory_raw_reports[1]
1707 .descriptor
1708 .canonical_realpath = duplicate_path.clone();
1709 duplicate.seal_raw_reports[1].absolute_path = duplicate_path.clone();
1710 duplicate.seal_raw_reports[1].descriptor.canonical_realpath = duplicate_path;
1711 assert_rejected(duplicate, FailureCode::IdentityJoin);
1712
1713 let mut outside_root = fixture(&request);
1714 outside_root.inventory_raw_reports[0].absolute_path = "/other/report-a.raw".to_owned();
1715 outside_root.inventory_raw_reports[0]
1716 .descriptor
1717 .canonical_realpath = "/other/report-a.raw".to_owned();
1718 outside_root.seal_raw_reports[0].absolute_path = "/other/report-a.raw".to_owned();
1719 outside_root.seal_raw_reports[0]
1720 .descriptor
1721 .canonical_realpath = "/other/report-a.raw".to_owned();
1722 assert_rejected(outside_root, FailureCode::IdentityJoin);
1723
1724 let mut seal_digest_splice = fixture(&request);
1725 seal_digest_splice.seal_raw_reports[0].sha256 = "f".repeat(64);
1726 assert_rejected(seal_digest_splice, FailureCode::IdentityJoin);
1727
1728 let mut primary_alias = fixture(&request);
1729 primary_alias.inventory_raw_reports[0] = xclbin_artifact();
1730 primary_alias.seal_raw_reports[0] = xclbin_artifact();
1731 assert_rejected(primary_alias, FailureCode::IdentityJoin);
1732
1733 let mut zero_length = fixture(&request);
1734 zero_length.inventory_raw_reports[0].bytes = 0;
1735 zero_length.seal_raw_reports[0].bytes = 0;
1736 assert_rejected(zero_length, FailureCode::Noncanonical);
1737 }
1738
1739 #[test]
1740 fn xclbin_identity_mutations_fail_closed() {
1741 let request = synthetic_request(&"1".repeat(32));
1742
1743 let mut uppercase_uuid = fixture(&request);
1744 uppercase_uuid.xclbin_identity.uuid = XCLBIN_UUID.to_ascii_uppercase();
1745 assert_rejected(uppercase_uuid, FailureCode::Noncanonical);
1746
1747 let mut canonical_uuid_substitution = fixture(&request);
1748 canonical_uuid_substitution.xclbin_identity.uuid =
1749 "22222222-3333-4444-5555-666666666666".to_owned();
1750 assert_rejected(canonical_uuid_substitution, FailureCode::IdentityJoin);
1751
1752 let mut zero_interface_uuid = fixture(&request);
1753 zero_interface_uuid.xclbin_identity.interface_uuid =
1754 "00000000-0000-0000-0000-000000000000".to_owned();
1755 assert_rejected(zero_interface_uuid, FailureCode::Noncanonical);
1756
1757 let mut canonical_interface_substitution = fixture(&request);
1758 canonical_interface_substitution
1759 .xclbin_identity
1760 .interface_uuid = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff".to_owned();
1761 assert_rejected(canonical_interface_substitution, FailureCode::IdentityJoin);
1762
1763 let mut wrong_platform = fixture(&request);
1764 wrong_platform.xclbin_identity.deployment_platform = "other-platform".to_owned();
1765 assert_rejected(wrong_platform, FailureCode::IdentityJoin);
1766
1767 let mut wrong_part = fixture(&request);
1768 wrong_part.xclbin_identity.fpga_part = "other-part".to_owned();
1769 assert_rejected(wrong_part, FailureCode::IdentityJoin);
1770
1771 let mut wrong_target = fixture(&request);
1772 wrong_target.xclbin_identity.target = "hw_emu".to_owned();
1773 assert_rejected(wrong_target, FailureCode::RouteUnaccepted);
1774
1775 let mut subject_splice = fixture(&request);
1776 subject_splice.xclbin_identity = RouteXclbinIdentity::from_exact_parser(
1777 subject(
1778 "/evidence/route-acceptance/output/other-kernel.xclbin",
1779 1_001,
1780 "f".repeat(64),
1781 41,
1782 ),
1783 XCLBIN_UUID.to_owned(),
1784 INTERFACE_UUID.to_owned(),
1785 REQUIRED_PLATFORM.to_owned(),
1786 REQUIRED_DEVICE_PART.to_owned(),
1787 REQUIRED_XCLBIN_TARGET.to_owned(),
1788 )
1789 .unwrap();
1790 assert_rejected(subject_splice, FailureCode::IdentityJoin);
1791 }
1792
1793 #[test]
1794 fn route_tier_cannot_prepopulate_any_terminal_claim() {
1795 let request = synthetic_request(&"1".repeat(32));
1796
1797 let mut hardware = fixture(&request);
1798 hardware.nonclaims.hardware_execution = true;
1799 assert_rejected(hardware, FailureCode::PrematureClaim);
1800
1801 let mut card = fixture(&request);
1802 card.nonclaims.card_validated = true;
1803 assert_rejected(card, FailureCode::PrematureClaim);
1804
1805 let mut crypto = fixture(&request);
1806 crypto.nonclaims.cryptographic_outputs_valid = true;
1807 assert_rejected(crypto, FailureCode::PrematureClaim);
1808
1809 let mut performance = fixture(&request);
1810 performance.nonclaims.performance_validated = true;
1811 assert_rejected(performance, FailureCode::PrematureClaim);
1812
1813 let mut completion = fixture(&request);
1814 completion.nonclaims.hardware_completion_promotable = true;
1815 assert_rejected(completion, FailureCode::PrematureClaim);
1816 }
1817}