44
55use std:: convert:: TryInto ;
66use std:: fs:: File ;
7- use std:: net:: { IpAddr , Ipv4Addr , SocketAddr } ;
87use std:: num:: { NonZeroU8 , NonZeroUsize } ;
98use std:: os:: unix:: fs:: FileTypeExt ;
109use std:: sync:: Arc ;
@@ -25,6 +24,9 @@ use crucible_client_types::VolumeConstructionRequest;
2524pub use nexus_client:: Client as NexusClient ;
2625use oximeter:: types:: ProducerRegistry ;
2726use oximeter_instruments:: kstat:: KstatSampler ;
27+ use propolis:: attestation;
28+ use propolis:: attestation:: server:: AttestationServerConfig ;
29+ use propolis:: attestation:: server:: AttestationSock ;
2830use propolis:: block;
2931use propolis:: chardev:: { self , BlockingSource , Source } ;
3032use propolis:: common:: { Lifecycle , GB , MB , PAGE_SIZE } ;
@@ -96,6 +98,12 @@ pub enum MachineInitError {
9698 #[ error( "boot order entry {0:?} does not refer to an attached disk" ) ]
9799 BootOrderEntryWithoutDevice ( SpecKey ) ,
98100
101+ #[ error(
102+ "disk device {device_id:?} refers to a \
103+ non-existent block backend {backend_id:?}"
104+ ) ]
105+ DeviceWithoutBlockBackend { device_id : SpecKey , backend_id : SpecKey } ,
106+
99107 #[ error( "boot entry {0:?} refers to a device on non-zero PCI bus {1}" ) ]
100108 BootDeviceOnDownstreamPciBus ( SpecKey , u8 ) ,
101109
@@ -105,6 +113,9 @@ pub enum MachineInitError {
105113 #[ error( "failed to specialize CPUID for vcpu {0}" ) ]
106114 CpuidSpecializationFailed ( i32 , #[ source] propolis:: cpuid:: SpecializeError ) ,
107115
116+ #[ error( "failed to start attestation server" ) ]
117+ AttestationServer ( #[ source] std:: io:: Error ) ,
118+
108119 #[ cfg( feature = "falcon" ) ]
109120 #[ error( "softnpu p9 device missing" ) ]
110121 SoftNpuP9Missing ,
@@ -478,31 +489,25 @@ impl MachineInitializer<'_> {
478489 Ok ( ( ) )
479490 }
480491
481- pub fn initialize_vsock (
492+ pub async fn initialize_vsock (
482493 & mut self ,
483494 chipset : & RegisteredChipset ,
484- ) -> Result < ( ) , MachineInitError > {
495+ attest_cfg : Option < AttestationServerConfig > ,
496+ ) -> Result < Option < AttestationSock > , MachineInitError > {
485497 use propolis:: vsock:: proxy:: VsockPortMapping ;
486498
487- // OANA Port 605 - VM Attestation RFD 605
488- const ATTESTATION_PORT : u16 = 605 ;
489- const ATTESTATION_ADDR : SocketAddr = SocketAddr :: new (
490- IpAddr :: V4 ( Ipv4Addr :: new ( 127 , 0 , 0 , 1 ) ) ,
491- ATTESTATION_PORT ,
492- ) ;
493-
494499 if let Some ( vsock) = & self . spec . vsock {
495500 let bdf: pci:: Bdf = vsock. spec . pci_path . into ( ) ;
496501
497502 let mappings = vec ! [ VsockPortMapping :: new(
498- ATTESTATION_PORT . into( ) ,
499- ATTESTATION_ADDR ,
503+ attestation :: ATTESTATION_PORT . into( ) ,
504+ attestation :: ATTESTATION_ADDR ,
500505 ) ] ;
501506
502507 let guest_cid = GuestCid :: try_from ( vsock. spec . guest_cid )
503- . context ( "guest cid" ) ?;
508+ . context ( "could not parse guest cid" ) ?;
504509 // While the spec does not recommend how large the virtio descriptor
505- // table should be we sized this appropriately in testing so
510+ // table should be, we sized this appropriately in testing, so
506511 // that the guest is able to move vsock packets at a reasonable
507512 // throughput without the need to be much larger.
508513 let num_queues = 256 ;
@@ -516,9 +521,23 @@ impl MachineInitializer<'_> {
516521
517522 self . devices . insert ( vsock. id . clone ( ) , device. clone ( ) ) ;
518523 chipset. pci_attach ( bdf, device) ;
524+
525+ // Spawn attestation server that will go over the vsock device
526+ if let Some ( cfg) = attest_cfg {
527+ let attest = AttestationSock :: new (
528+ self . log . new ( slog:: o!( "component" => "attestation-server" ) ) ,
529+ cfg. sled_agent_addr ,
530+ )
531+ . await
532+ . map_err ( MachineInitError :: AttestationServer ) ?;
533+ return Ok ( Some ( attest) ) ;
534+ }
535+ } else {
536+ info ! ( self . log, "no vsock device in instance spec" ) ;
537+ return Ok ( None ) ;
519538 }
520539
521- Ok ( ( ) )
540+ Ok ( None )
522541 }
523542
524543 async fn create_storage_backend_from_spec (
@@ -672,6 +691,99 @@ impl MachineInitializer<'_> {
672691 }
673692 }
674693
694+ /// Collect the necessary information out of the VM under construction into
695+ /// the provided `AttestationSocketInit`. This is expected to populate
696+ /// `attest_init` with information so the caller can spawn off
697+ /// `AttestationSockInit::run`.
698+ pub fn prepare_rot_initializer (
699+ & self ,
700+ vm_rot : & mut AttestationSock ,
701+ ) -> Result < ( ) , MachineInitError > {
702+ let uuid = self . properties . id ;
703+
704+ // The first boot entry is a key into `self.spec.disks`, which is how
705+ // we'll get to a Crucible volume backing this boot option.
706+ let boot_disk_entry =
707+ self . spec . boot_settings . as_ref ( ) . and_then ( |settings| {
708+ if settings. order . len ( ) >= 2 {
709+ // In a rack we only configure propolis-server with zero or
710+ // one boot disks. It's possible to provide a fuller list,
711+ // and in the future the product may actually expose such a
712+ // capability. At that time, we'll need to have a reckoning
713+ // for what "boot disk measurement" from the RoT actually
714+ // means; it probably "should" be "the measurement of the
715+ // disk that EDK2 decided to boot into", but that
716+ // communication to and from the guest is a little more
717+ // complicated than we want or need to build out today.
718+ //
719+ // Since as the system exists we either have no specific
720+ // boot disk (and don't know where the guest is expected to
721+ // end up), or one boot disk (and can determine which disk
722+ // to collect a measurement of before even running guest
723+ // firmware), we encode this expectation up front. If the
724+ // product has changed such that this assert is reached,
725+ // "that's exciting!" and "sorry for crashing your
726+ // Propolis".
727+ panic ! (
728+ "Unsupported VM RoT configuration: \
729+ more than one boot disk"
730+ ) ;
731+ }
732+
733+ settings. order . first ( )
734+ } ) ;
735+
736+ let crucible_volume = if let Some ( entry) = boot_disk_entry {
737+ let disk_dev =
738+ self . spec . disks . get ( & entry. device_id ) . ok_or_else ( || {
739+ MachineInitError :: BootOrderEntryWithoutDevice (
740+ entry. device_id . clone ( ) ,
741+ )
742+ } ) ?;
743+
744+ let backend_id = match & disk_dev. device_spec {
745+ spec:: StorageDevice :: Virtio ( disk) => & disk. backend_id ,
746+ spec:: StorageDevice :: Nvme ( disk) => & disk. backend_id ,
747+ } ;
748+
749+ let Some ( block_backend) = self . block_backends . get ( backend_id)
750+ else {
751+ return Err ( MachineInitError :: DeviceWithoutBlockBackend {
752+ device_id : entry. device_id . to_owned ( ) ,
753+ backend_id : backend_id. to_owned ( ) ,
754+ } ) ;
755+ } ;
756+
757+ if let Some ( backend) =
758+ block_backend. as_any ( ) . downcast_ref :: < block:: CrucibleBackend > ( )
759+ {
760+ if backend. is_read_only ( ) {
761+ Some ( backend. clone_volume ( ) )
762+ } else {
763+ // Disk must be read-only to be used for attestation.
764+ slog:: info!(
765+ self . log,
766+ "boot disk is not read-only (and will not be used for attestations)" ,
767+ ) ;
768+ None
769+ }
770+ } else {
771+ // Probably fine, just not handled right now.
772+ slog:: warn!(
773+ self . log,
774+ "VM RoT ignoring boot disk: not a Crucible volume"
775+ ) ;
776+ None
777+ }
778+ } else {
779+ None
780+ } ;
781+
782+ vm_rot. prepare_instance_conf ( uuid, crucible_volume) ;
783+
784+ Ok ( ( ) )
785+ }
786+
675787 /// Initializes the storage devices and backends listed in this
676788 /// initializer's instance spec.
677789 ///
0 commit comments