diff --git a/rs/ic_os/os_tools/guest_disk/src/crypt.rs b/rs/ic_os/os_tools/guest_disk/src/crypt.rs index 976531e76c24..16ae1c9ef118 100644 --- a/rs/ic_os/os_tools/guest_disk/src/crypt.rs +++ b/rs/ic_os/os_tools/guest_disk/src/crypt.rs @@ -26,14 +26,22 @@ const PBKDF_ITERATIONS: u32 = 1000; pub const LUKS2_N_KEYSLOTS: u32 = 32; /// Number of tokens supported by LUKS2 pub const LUKS2_N_TOKENS: u32 = 32; +/// LUKS2 token type identifier for our key slot metadata. pub const IC_KEY_TOKEN_TYPE: &str = "ic-key-metadata"; +/// All encrypted partitions contain a single active keyslot with maximum 1 active token. +/// (SEV-based derivation has 1 token, generated keys have no associated tokens) +pub const SINGLE_KEYSLOT_INDEX: u32 = 0; +pub const SINGLE_TOKEN_INDEX: u32 = 0; + #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KeyslotMetadata { - // token_type and keyslots are standard fields in LUKS tokens +pub struct KeyslotToken { + /// LUKS2 token type — must be set to [`IC_KEY_TOKEN_TYPE`]. #[serde(rename = "type")] token_type: String, + /// Key slots this token is associated with (LUKS2 requires this field). pub keyslots: Vec, + /// Metadata used to derive the key this token refers to. pub sev_metadata: SevMetadata, // Note: this type is serialized and stored on disk. When adding a new field, make sure to // set the type to Optional or mark it with #[serde(default)]. @@ -47,26 +55,14 @@ pub struct SevMetadata { // set the type to Optional or mark it with #[serde(default)]. } -impl KeyslotMetadata { - pub fn new_sev(keyslot: u32, sev_metadata: SevMetadata) -> Self { +impl KeyslotToken { + pub fn new_sev(sev_metadata: SevMetadata) -> Self { Self { token_type: IC_KEY_TOKEN_TYPE.to_string(), - keyslots: vec![keyslot.to_string()], + keyslots: vec![SINGLE_KEYSLOT_INDEX.to_string()], sev_metadata, } } - - pub fn keyslot(&self) -> Result { - if self.keyslots.len() != 1 { - bail!( - "Token must have exactly one keyslot, but found {:?}", - self.keyslots - ); - } - self.keyslots[0] - .parse() - .context("Keyslot is not a valid number") - } } #[derive(Debug)] @@ -135,9 +131,8 @@ fn obtain_crypt_device_handle_with_detached_header( .context("Failed to initialize cryptographic device with detached header") } -/// Initializes a cryptographic device at the specified path with LUKS2 format and activates it -/// using the provided name and encryption key. -/// Returns the crypt device and the activated keyslot. +/// Activates the cryptographic device at the specified path under the given name, +/// using the provided encryption key. pub fn activate_crypt_device( device_path: &Path, header_location: LuksHeaderLocation, @@ -146,7 +141,7 @@ pub fn activate_crypt_device( flags: CryptActivate, verify_luks_params: bool, metrics_registry: Option<&Registry>, -) -> Result<(CryptDevice, u32)> { +) -> Result<()> { let mut crypt_device = open_luks2_device(device_path, header_location)?; let luks_parameters = extract_luks_parameters(&mut crypt_device); @@ -166,7 +161,7 @@ pub fn activate_crypt_device( } } - Ok((crypt_device, active_keyslot)) + Ok(()) } /// Deactivates the cryptographic device with the given name. @@ -207,8 +202,8 @@ fn apply_default_settings(crypt_device: &mut CryptDevice) -> Result<()> { pub fn format_crypt_device( device_path: &Path, header_location: LuksHeaderLocation, - encryption_key: &[u8], -) -> Result<(CryptDevice, u32)> { + passphrase: &[u8], +) -> Result { if let LuksHeaderLocation::Detached(header_path) = header_location { File::create(header_path) .context("Failed to create detached LUKS header file")? @@ -236,12 +231,17 @@ pub fn format_crypt_device( None, ) .context("Failed to call format")?; - let keyslot = crypt_device + crypt_device .keyslot_handle() - .add_by_key(None, None, encryption_key, CryptVolumeKey::empty()) + .add_by_key( + Some(SINGLE_KEYSLOT_INDEX), + None, + passphrase, + CryptVolumeKey::empty(), + ) .context("Could not add key to cryptographic device")?; - Ok((crypt_device, keyslot)) + Ok(crypt_device) } /// Opens a LUKS2 device at the specified path, loads its context, and prepares handle-local @@ -470,62 +470,41 @@ pub(crate) fn destroy_keyslots_except_first(crypt_device: &mut CryptDevice) -> R fn remove_all_tokens(crypt_device: &mut CryptDevice) -> Result<()> { for token_id in 0..LUKS2_N_TOKENS { - crypt_device - .token_handle() - .json_set(TokenInput::RemoveToken(token_id)) - .with_context(|| format!("Failed to remove IC key metadata token {token_id}"))?; + if !matches!( + crypt_device.token_handle().status(token_id), + Ok(CryptTokenInfo::Inactive) + ) { + crypt_device + .token_handle() + .json_set(TokenInput::RemoveToken(token_id)) + .with_context(|| format!("Failed to remove IC key metadata token {token_id}"))?; + } } Ok(()) } -fn ic_key_token_ids(crypt_device: &mut CryptDevice) -> Vec { - (0..LUKS2_N_TOKENS) - .filter(|&token_id| { - matches!( - crypt_device.token_handle().status(token_id), - Ok(CryptTokenInfo::External(ref token_type) - | CryptTokenInfo::ExternalUnknown(ref token_type)) - if token_type == IC_KEY_TOKEN_TYPE - ) - }) - .collect() -} - -pub fn read_keyslot_metadata(crypt_device: &mut CryptDevice) -> Result> { - ic_key_token_ids(crypt_device) - .into_iter() - .map(|token_id| { - let json = crypt_device - .token_handle() - .json_get(token_id) - .with_context(|| format!("Failed to read IC key metadata token {token_id}"))?; - let metadata = serde_json::from_value::(json) - .with_context(|| format!("Failed to parse IC key metadata token {token_id}"))?; - metadata - .keyslot() - .with_context(|| format!("Invalid keyslot in IC key metadata token {token_id}"))?; - Ok(metadata) - }) - .collect() +pub fn read_single_keyslot_token(crypt_device: &mut CryptDevice) -> Result { + let json = crypt_device + .token_handle() + .json_get(SINGLE_TOKEN_INDEX) + .with_context(|| format!("Failed to read IC key metadata token {SINGLE_TOKEN_INDEX}"))?; + serde_json::from_value::(json) + .with_context(|| format!("Failed to parse IC key metadata token {SINGLE_TOKEN_INDEX}")) } -pub fn add_sev_metadata( - crypt_device: &mut CryptDevice, - keyslot: u32, - sev_metadata: SevMetadata, -) -> Result<()> { +pub fn add_sev_metadata(crypt_device: &mut CryptDevice, sev_metadata: SevMetadata) -> Result<()> { // TODO: Legacy headers may carry more than one IC key metadata token. Once all nodes // have been updated (i.e., the num_tokens metric is 1 everywhere), this removal can // be deleted so that only one token is written. remove_all_tokens(crypt_device)?; - let json = serde_json::to_value(KeyslotMetadata::new_sev(keyslot, sev_metadata)) + let json = serde_json::to_value(KeyslotToken::new_sev(sev_metadata)) .context("Failed to serialize key slot metadata")?; crypt_device .token_handle() - .json_set(TokenInput::ReplaceToken(0, &json)) + .json_set(TokenInput::ReplaceToken(SINGLE_TOKEN_INDEX, &json)) .context("Failed to write LUKS2 token")?; Ok(()) diff --git a/rs/ic_os/os_tools/guest_disk/src/generated_key.rs b/rs/ic_os/os_tools/guest_disk/src/generated_key.rs index c7294af97caa..fda5d7dafe84 100644 --- a/rs/ic_os/os_tools/guest_disk/src/generated_key.rs +++ b/rs/ic_os/os_tools/guest_disk/src/generated_key.rs @@ -14,7 +14,7 @@ const GENERATED_KEY_SIZE_BYTES: usize = 16; pub struct GeneratedKeyDiskEncryption<'a> { pub key_path: &'a Path, - pub metrics_registry: &'a Registry, + pub metrics_registry: Registry, } impl DiskEncryption for GeneratedKeyDiskEncryption<'_> { @@ -32,7 +32,7 @@ impl DiskEncryption for GeneratedKeyDiskEncryption<'_> { // execution environment) /*verify_luks_params=*/ false, - Some(self.metrics_registry), + Some(&self.metrics_registry), ) .context("Failed to initialize crypt device")?; diff --git a/rs/ic_os/os_tools/guest_disk/src/main.rs b/rs/ic_os/os_tools/guest_disk/src/main.rs index 67c4552189b0..896388d507fd 100644 --- a/rs/ic_os/os_tools/guest_disk/src/main.rs +++ b/rs/ic_os/os_tools/guest_disk/src/main.rs @@ -88,7 +88,7 @@ fn run( } else { Box::new(GeneratedKeyDiskEncryption { key_path: generated_key_path, - metrics_registry: &metrics_registry, + metrics_registry: metrics_registry.clone(), }) }; let partition = args.partition(); diff --git a/rs/ic_os/os_tools/guest_disk/src/sev.rs b/rs/ic_os/os_tools/guest_disk/src/sev.rs index 28023a04fe9d..0f6170ab7bb3 100644 --- a/rs/ic_os/os_tools/guest_disk/src/sev.rs +++ b/rs/ic_os/os_tools/guest_disk/src/sev.rs @@ -1,6 +1,6 @@ use crate::crypt::{ - LuksHeaderLocation, SevMetadata, activate_crypt_device, add_sev_metadata, check_encryption_key, - destroy_keyslots_except_first, format_crypt_device, open_luks2_device, + LuksHeaderLocation, SINGLE_KEYSLOT_INDEX, SevMetadata, activate_crypt_device, add_sev_metadata, + check_encryption_key, destroy_keyslots_except_first, format_crypt_device, open_luks2_device, }; use crate::{DiskEncryption, Partition, activate_flags}; use anyhow::{Context, Result, bail}; @@ -22,6 +22,15 @@ pub struct SevDiskEncryption { pub metrics_registry: Registry, } +impl SevDiskEncryption { + fn header_location(&self, partition: Partition) -> LuksHeaderLocation<'_> { + match partition { + Partition::Store => LuksHeaderLocation::Detached(&self.store_luks_header_path), + Partition::Var => LuksHeaderLocation::Attached, + } + } +} + impl DiskEncryption for SevDiskEncryption { fn open(&mut self, device_path: &Path, partition: Partition, crypt_name: &str) -> Result<()> { let key = derive_key_from_sev_measurement( @@ -30,33 +39,16 @@ impl DiskEncryption for SevDiskEncryption { ) .context("Failed to derive SEV key for disk encryption")?; - match partition { - Partition::Var => { - activate_crypt_device( - device_path, - LuksHeaderLocation::Attached, - crypt_name, - key.as_bytes(), - activate_flags(partition), - /*verify_luks_params=*/ true, - Some(&self.metrics_registry), - ) - .context("Failed to open crypt device for var partition")?; - } - - Partition::Store => { - activate_crypt_device( - device_path, - LuksHeaderLocation::Detached(&self.store_luks_header_path), - crypt_name, - key.as_bytes(), - activate_flags(partition), - /*verify_luks_params=*/ true, - Some(&self.metrics_registry), - ) - .context("Failed to initialize crypt device for store partition")?; - } - } + activate_crypt_device( + device_path, + self.header_location(partition), + crypt_name, + key.as_bytes(), + activate_flags(partition), + /*verify_luks_params=*/ true, + Some(&self.metrics_registry), + ) + .with_context(|| format!("Failed to open the {partition:?} partition"))?; Ok(()) } @@ -70,24 +62,18 @@ impl DiskEncryption for SevDiskEncryption { let sev_metadata = get_sev_metadata_for_luks(self.sev_firmware.as_mut())?; - let header_location = match partition { - Partition::Store => { - if self.store_luks_header_path.exists() { - bail!( - "Refusing to format Store because detached LUKS header {} already exists. \ - Remove the stale header first if you really want to reformat the device.", - self.store_luks_header_path.display() - ); - } - LuksHeaderLocation::Detached(&self.store_luks_header_path) - } - Partition::Var => LuksHeaderLocation::Attached, - }; + if partition == Partition::Store && self.store_luks_header_path.exists() { + bail!( + "Refusing to format Store because detached LUKS header {} already exists. \ + Remove the stale header first if you really want to reformat the device.", + self.store_luks_header_path.display() + ); + } - let (mut crypt_device, keyslot) = - format_crypt_device(device_path, header_location, key.as_bytes()) + let mut crypt_device = + format_crypt_device(device_path, self.header_location(partition), key.as_bytes()) .context("Failed to format partition")?; - add_sev_metadata(&mut crypt_device, keyslot, sev_metadata) + add_sev_metadata(&mut crypt_device, sev_metadata) .context("Failed to write SEV keyslot metadata")?; Ok(()) @@ -146,12 +132,19 @@ pub fn rekey( // new key. Fails if the old key does not unlock any keyslot. crypt_device .keyslot_handle() - .change_by_passphrase(None, Some(0), old_key, new_key.as_bytes()) + .change_by_passphrase( + // TODO: after all nodes have a single keyslot at SINGLE_KEYSLOT_INDEX, change this to + // Some(SINGLE_KEYSLOT_INDEX) + None, + Some(SINGLE_KEYSLOT_INDEX), + old_key, + new_key.as_bytes(), + ) .context("Failed to replace the received key with the new SEV-derived key")?; // Removes the keyslots that legacy headers may still carry. // TODO: remove it (see comment on destroy_keyslots_except_first). destroy_keyslots_except_first(&mut crypt_device)?; - add_sev_metadata(&mut crypt_device, 0, sev_metadata) + add_sev_metadata(&mut crypt_device, sev_metadata) .context("Failed to write SEV keyslot metadata")?; Ok(()) diff --git a/rs/ic_os/os_tools/guest_disk/src/tests.rs b/rs/ic_os/os_tools/guest_disk/src/tests.rs index 129482e12178..0476c307dde2 100644 --- a/rs/ic_os/os_tools/guest_disk/src/tests.rs +++ b/rs/ic_os/os_tools/guest_disk/src/tests.rs @@ -17,8 +17,9 @@ use crate::{Args, Partition, crypt_name, metrics_file_path, run}; use anyhow::{Result, anyhow}; use guest_disk::DiskEncryption; use guest_disk::crypt::{ - IC_KEY_TOKEN_TYPE, KeyslotMetadata, LUKS2_N_KEYSLOTS, LUKS2_N_TOKENS, LuksHeaderLocation, - deactivate_crypt_device, format_crypt_device, open_luks2_device, read_keyslot_metadata, + IC_KEY_TOKEN_TYPE, KeyslotToken, LUKS2_N_KEYSLOTS, LUKS2_N_TOKENS, LuksHeaderLocation, + SINGLE_KEYSLOT_INDEX, SINGLE_TOKEN_INDEX, SevMetadata, deactivate_crypt_device, + format_crypt_device, open_luks2_device, read_single_keyslot_token, }; use guest_disk::sev::{SevDiskEncryption, can_open, rekey}; use ic_device::device_mapping::{Bytes, TempDevice}; @@ -27,7 +28,7 @@ use itertools::Either::Right; use libcryptsetup_rs::consts::flags::CryptVolumeKey; use libcryptsetup_rs::consts::vals::{CryptKdf, EncryptionFormat, KeyslotInfo}; use libcryptsetup_rs::{ - CryptDevice, CryptInit, CryptParamsLuks2Ref, CryptSettingsHandle, CryptTokenInfo, + CryptDevice, CryptInit, CryptParamsLuks2Ref, CryptSettingsHandle, CryptTokenInfo, TokenInput, }; use prometheus::Registry; use sev::Generation; @@ -166,40 +167,26 @@ impl<'a> PartitionView<'a> { } } - /// Reads all `ic-key-metadata` tokens from the device, verifying in passing that no - /// unexpected (internal or invalid) tokens are present. - fn read_keyslot_metadata(&self) -> Vec { + /// Asserts that there is only a single token at index 0 and returns it. + fn read_keyslot_token(&self) -> KeyslotToken { + self.assert_single_metadata_token(); + let token = read_single_keyslot_token(&mut self.open_crypt_device()).unwrap(); + assert_eq!(token.keyslots, [SINGLE_KEYSLOT_INDEX.to_string()]); + token + } + + /// Asserts that the device carries no token at all. + fn assert_no_metadata_token(&self) { let mut crypt_device = self.open_crypt_device(); - let mut expected_token_count = 0; - // Verify that only our tokens are present. There is no reason for any other token type - // to be present. for token_id in 0..LUKS2_N_TOKENS { - match crypt_device.token_handle().status(token_id).unwrap() { - CryptTokenInfo::Invalid => { - panic!("expected token {token_id} to be valid"); - } - CryptTokenInfo::Inactive => { /* no-op */ } - CryptTokenInfo::Internal(_) | CryptTokenInfo::InternalUnknown(_) => { - panic!("Did not expect internal token {token_id}") - } - CryptTokenInfo::External(_) | CryptTokenInfo::ExternalUnknown(_) => { - expected_token_count += 1; - } - } - } - - let metadata = read_keyslot_metadata(&mut crypt_device).unwrap(); - assert_eq!( - metadata.len(), - expected_token_count, - "expected to read all ic-key-metadata tokens from the device" - ); - - for entry in &metadata { - entry.keyslot().expect("expected keyslot to be present"); + assert!( + matches!( + crypt_device.token_handle().status(token_id).unwrap(), + CryptTokenInfo::Inactive + ), + "did not expect token {token_id} on the device" + ); } - - metadata } /// Asserts the single-token invariant: the only token on the device is the @@ -209,7 +196,7 @@ impl<'a> PartitionView<'a> { let mut crypt_device = self.open_crypt_device(); for token_id in 0..LUKS2_N_TOKENS { let status = crypt_device.token_handle().status(token_id).unwrap(); - let expected = if token_id == 0 { + let expected = if token_id == SINGLE_TOKEN_INDEX { matches!( status, CryptTokenInfo::ExternalUnknown(ref token_type) @@ -610,11 +597,8 @@ fn test_generated_key_init_and_reopen() { "detached Store header should not exist for {partition_name:?} with generated key" ); } - assert_eq!( - partition.read_keyslot_metadata().len(), - 0, - "Unexpected keyslot metadata when using generated key for {partition_name:?}" - ); + // Generated-key partitions carry no metadata token. + partition.assert_no_metadata_token(); } } @@ -680,26 +664,17 @@ fn test_sev_key_init_and_reopen() { } #[test] -fn test_sev_format_writes_keyslot_metadata() { +fn test_sev_format_writes_keyslot_token() { for partition in [Partition::Store, Partition::Var] { let fixture = TestFixture::new_sev(); fixture.partition(partition).format().unwrap(); - let metadata = fixture.partition(partition).read_keyslot_metadata(); - assert_eq!( - metadata.len(), - 1, - "expected one metadata token for {partition:?}" - ); - fixture.partition(partition).assert_single_metadata_token(); + let token = fixture.partition(partition).read_keyslot_token(); assert_eq!( - metadata[0].sev_metadata.launch_measurement_hex, + token.sev_metadata.launch_measurement_hex, default_launch_measurement_as_hex() ); - assert_eq!( - metadata[0].sev_metadata.tcb_version, - default_launch_tcb_as_u64() - ); + assert_eq!(token.sev_metadata.tcb_version, default_launch_tcb_as_u64()); } } @@ -930,15 +905,12 @@ fn test_open_store_multiple_times_with_different_keys() { // Each re-key replaces the old key in place: the single keyslot (always the first) // and the single metadata token (always the first) carry the newest GuestOS's key // and launch measurement. - let metadata = fixture.store_partition().read_keyslot_metadata(); - assert_eq!(metadata.len(), 1); - assert_eq!(metadata[0].keyslot().unwrap(), 0); + let token = fixture.store_partition().read_keyslot_token(); assert_eq!( - metadata[0].sev_metadata.launch_measurement_hex, + token.sev_metadata.launch_measurement_hex, hex::encode([5_u8; 48]) ); assert_eq!(fixture.store_partition().active_keyslot_count(), 1); - fixture.store_partition().assert_single_metadata_token(); } /// A legacy header carrying an extra (stale) keyslot converges back to the single first @@ -953,7 +925,7 @@ fn test_upgrade_removes_stale_keyslots() { // Build the legacy layout: the previous GuestOS's key in the first keyslot, the // current GuestOS's (served) key in a later one. let served_key = fixture.derive_sev_key(Partition::Store); - let (mut crypt_device, _) = format_crypt_device( + let mut crypt_device = format_crypt_device( fixture.store_device_path(), LuksHeaderLocation::Detached(&fixture.store_header_path()), STALE_KEY, @@ -971,10 +943,90 @@ fn test_upgrade_removes_stale_keyslots() { .expect("opening Store after the upgrade should succeed"); assert_eq!(fixture.store_partition().active_keyslot_count(), 1); - let metadata = fixture.store_partition().read_keyslot_metadata(); - assert_eq!(metadata.len(), 1); - assert_eq!(metadata[0].keyslot().unwrap(), 0); - fixture.store_partition().assert_single_metadata_token(); + // The re-key converges the legacy header back to the single token in the first + // position, assigned to the single keyslot. + fixture.store_partition().read_keyslot_token(); +} + +/// A legacy header carrying its keyslot at a non-zero index and its IC key metadata +/// token at a non-zero index migrates on the next upgrade: the re-key succeeds and +/// converges to the canonical layout with a single keyslot at index 0 and a single +/// token at index 0. +// TODO: remove this test once all nodes only use a single key slot + token per device +#[test] +fn test_rekey_migrates_legacy_keyslot_and_token_positions() { + const LEGACY_KEYSLOT_INDEX: u32 = 2; + const LEGACY_TOKEN_INDEX: u32 = 3; + const STALE_KEY: &[u8] = b"stale previous key"; + + let mut fixture = TestFixture::new_sev(); + + // Build the legacy layout: the current GuestOS's (served) key in keyslot 2, keyslot + // 0 unused, and the IC key metadata token in token position 3. + let served_key = fixture.derive_sev_key(Partition::Store); + let mut crypt_device = format_crypt_device( + fixture.store_device_path(), + LuksHeaderLocation::Detached(&fixture.store_header_path()), + STALE_KEY, + ) + .unwrap(); + crypt_device + .keyslot_handle() + .add_by_passphrase(Some(LEGACY_KEYSLOT_INDEX), STALE_KEY, &served_key) + .expect("Failed to add the served key's keyslot at the legacy position"); + crypt_device + .keyslot_handle() + .destroy(SINGLE_KEYSLOT_INDEX) + .expect("Failed to remove the format key's keyslot"); + let mut legacy_token = KeyslotToken::new_sev(SevMetadata { + launch_measurement_hex: default_launch_measurement_as_hex(), + tcb_version: default_launch_tcb_as_u64(), + }); + legacy_token.keyslots = vec![LEGACY_KEYSLOT_INDEX.to_string()]; + crypt_device + .token_handle() + .json_set(TokenInput::ReplaceToken( + LEGACY_TOKEN_INDEX, + &serde_json::to_value(legacy_token).unwrap(), + )) + .expect("Failed to write the legacy IC key metadata token at position 3"); + drop(crypt_device); + + // Sanity-check the legacy layout before the migration: exactly one active keyslot + // (in position 2) and the IC key token in position 3. + assert_eq!(fixture.store_partition().active_keyslot_count(), 1); + assert!(matches!( + fixture + .store_partition() + .open_crypt_device() + .token_handle() + .status(LEGACY_TOKEN_INDEX) + .unwrap(), + CryptTokenInfo::ExternalUnknown(ref token_type) if token_type == IC_KEY_TOKEN_TYPE + )); + + fixture + .upgrade_sev_guestos_to([0x33; 48]) + .expect("re-keying a legacy header with non-zero keyslot/token positions should succeed"); + + // The migrated header carries exactly one active keyslot, and it is at index 0. + assert_eq!(fixture.store_partition().active_keyslot_count(), 1); + assert!(matches!( + fixture + .store_partition() + .open_crypt_device() + .keyslot_handle() + .status(SINGLE_KEYSLOT_INDEX) + .unwrap(), + KeyslotInfo::Active | KeyslotInfo::ActiveLast + )); + // The single token moved to position 0 referencing keyslot 0 (asserted by + // read_keyslot_token) and carries the new GuestOS's launch measurement. + let token = fixture.store_partition().read_keyslot_token(); + assert_eq!( + token.sev_metadata.launch_measurement_hex, + hex::encode([0x33_u8; 48]) + ); } #[test]