@@ -298,6 +298,12 @@ pub struct VmConfig {
298298 /// `tdx_attestation_variant = "lite"` and omitted for legacy TDX.
299299 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
300300 pub tdx_measurement : Option < TdxOsImageMeasurementDocument > ,
301+ /// GCP TDX no-image-download measurement material. Present for GCP
302+ /// deployments so `os_image_hash` can remain the unified image digest
303+ /// (`sha256(sha256sum.txt)`) while the verifier still binds the TPM UKI
304+ /// Authenticode event to that digest.
305+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
306+ pub gcp_measurement : Option < GcpOsImageMeasurementDocument > ,
301307}
302308
303309/// One OVMF SEV metadata section (gpa/size/type) that affects the SEV-SNP
@@ -331,6 +337,7 @@ fn sha256(bytes: &[u8]) -> [u8; 32] {
331337
332338pub const TDX_MEASUREMENT_FILENAME : & str = "measurement.tdx.cbor" ;
333339pub const SNP_MEASUREMENT_FILENAME : & str = "measurement.snp.cbor" ;
340+ pub const GCP_MEASUREMENT_FILENAME : & str = "measurement.gcp.cbor" ;
334341
335342pub fn image_hash_from_sha256sum ( checksum_file : & [ u8 ] ) -> [ u8 ; 32 ] {
336343 sha256 ( checksum_file)
@@ -401,6 +408,130 @@ pub fn verify_measurement_material(
401408 Ok ( ( ) )
402409}
403410
411+ /// Image-invariant GCP TDX measurement material. GCP's TPM event log measures
412+ /// the UKI as a PE/COFF Authenticode SHA-256 digest. The unified image identity
413+ /// remains `sha256(sha256sum.txt)`; this material is bound to that identity by
414+ /// the `measurement.gcp.cbor` entry in `sha256sum.txt`.
415+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
416+ pub struct GcpOsImageMeasurement {
417+ #[ serde( with = "hex_bytes" ) ]
418+ pub uki_authenticode_sha256 : Vec < u8 > ,
419+ }
420+
421+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
422+ struct CborGcpOsImageMeasurement {
423+ version : u32 ,
424+ #[ serde( rename = "uki_auth" , with = "hex_bytes" ) ]
425+ uki_authenticode_sha256 : Vec < u8 > ,
426+ }
427+
428+ impl From < & GcpOsImageMeasurement > for CborGcpOsImageMeasurement {
429+ fn from ( measurement : & GcpOsImageMeasurement ) -> Self {
430+ Self {
431+ version : GcpOsImageMeasurement :: VERSION ,
432+ uki_authenticode_sha256 : measurement. uki_authenticode_sha256 . clone ( ) ,
433+ }
434+ }
435+ }
436+
437+ impl From < CborGcpOsImageMeasurement > for GcpOsImageMeasurement {
438+ fn from ( measurement : CborGcpOsImageMeasurement ) -> Self {
439+ Self {
440+ uki_authenticode_sha256 : measurement. uki_authenticode_sha256 ,
441+ }
442+ }
443+ }
444+
445+ impl GcpOsImageMeasurement {
446+ pub const VERSION : u32 = 1 ;
447+ pub const UKI_AUTHENTICODE_SHA256_LEN : usize = 32 ;
448+
449+ pub fn new ( uki_authenticode_sha256 : Vec < u8 > ) -> Result < Self , String > {
450+ if uki_authenticode_sha256. len ( ) != Self :: UKI_AUTHENTICODE_SHA256_LEN {
451+ return Err ( format ! (
452+ "GcpOsImageMeasurement: UKI Authenticode hash has invalid length {}, expected {}" ,
453+ uki_authenticode_sha256. len( ) ,
454+ Self :: UKI_AUTHENTICODE_SHA256_LEN
455+ ) ) ;
456+ }
457+ Ok ( Self {
458+ uki_authenticode_sha256,
459+ } )
460+ }
461+
462+ pub fn to_cbor_vec ( & self ) -> Vec < u8 > {
463+ cbor_to_vec (
464+ & CborGcpOsImageMeasurement :: from ( self ) ,
465+ "GcpOsImageMeasurement" ,
466+ )
467+ }
468+
469+ pub fn from_cbor_slice ( bytes : & [ u8 ] ) -> Result < Self , String > {
470+ let measurement: CborGcpOsImageMeasurement =
471+ cbor_from_slice ( bytes, "GcpOsImageMeasurement" ) ?;
472+ if measurement. version != Self :: VERSION {
473+ return Err ( format ! (
474+ "GcpOsImageMeasurement unsupported version {}, expected {}" ,
475+ measurement. version,
476+ Self :: VERSION
477+ ) ) ;
478+ }
479+ Self :: new ( measurement. uki_authenticode_sha256 )
480+ }
481+
482+ pub fn cbor_json_value_from_slice ( bytes : & [ u8 ] ) -> Result < serde_json:: Value , String > {
483+ let measurement: CborGcpOsImageMeasurement =
484+ cbor_from_slice ( bytes, "GcpOsImageMeasurement" ) ?;
485+ serde_json:: to_value ( measurement)
486+ . map_err ( |e| format ! ( "GcpOsImageMeasurement: failed to convert to JSON: {e}" ) )
487+ }
488+
489+ pub fn measurement_hash ( & self ) -> [ u8 ; 32 ] {
490+ sha256 ( & self . to_cbor_vec ( ) )
491+ }
492+ }
493+
494+ #[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
495+ pub struct GcpOsImageMeasurementDocument {
496+ /// Raw checksum file bytes (`sha256sum.txt`). `sha256(checksum_file)` is
497+ /// the unified `os_image_hash`.
498+ #[ serde( with = "serde_human_bytes::base64" ) ]
499+ pub checksum_file : Vec < u8 > ,
500+ /// Raw bytes of `measurement.gcp.cbor`.
501+ #[ serde( with = "serde_human_bytes::base64" ) ]
502+ pub measurement : Vec < u8 > ,
503+ }
504+
505+ impl GcpOsImageMeasurementDocument {
506+ pub fn new ( checksum_file : Vec < u8 > , measurement : Vec < u8 > ) -> Self {
507+ Self {
508+ checksum_file,
509+ measurement,
510+ }
511+ }
512+
513+ pub fn from_measurement ( checksum_file : Vec < u8 > , measurement : GcpOsImageMeasurement ) -> Self {
514+ Self :: new ( checksum_file, measurement. to_cbor_vec ( ) )
515+ }
516+
517+ pub fn decode_measurement ( & self ) -> Result < GcpOsImageMeasurement , String > {
518+ GcpOsImageMeasurement :: from_cbor_slice ( & self . measurement )
519+ }
520+
521+ pub fn decode_measurement_value ( & self ) -> Result < serde_json:: Value , String > {
522+ GcpOsImageMeasurement :: cbor_json_value_from_slice ( & self . measurement )
523+ }
524+
525+ pub fn verify ( & self , os_image_hash : & [ u8 ] ) -> Result < ( ) , String > {
526+ verify_measurement_material (
527+ os_image_hash,
528+ & self . checksum_file ,
529+ & self . measurement ,
530+ GCP_MEASUREMENT_FILENAME ,
531+ )
532+ }
533+ }
534+
404535#[ derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize ) ]
405536struct CborOvmfSection {
406537 gpa : u64 ,
@@ -796,6 +927,8 @@ pub struct OsImageMeasurementDocument {
796927 pub tdx : Option < TdxOsImageMeasurementDocument > ,
797928 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
798929 pub snp : Option < SevOsImageMeasurementDocument > ,
930+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
931+ pub gcp : Option < GcpOsImageMeasurementDocument > ,
799932}
800933
801934impl OsImageMeasurementDocument {
@@ -804,11 +937,13 @@ impl OsImageMeasurementDocument {
804937 pub fn new (
805938 tdx : Option < TdxOsImageMeasurementDocument > ,
806939 snp : Option < SevOsImageMeasurementDocument > ,
940+ gcp : Option < GcpOsImageMeasurementDocument > ,
807941 ) -> Self {
808942 Self {
809943 version : Self :: VERSION ,
810944 tdx,
811945 snp,
946+ gcp,
812947 }
813948 }
814949}
0 commit comments