aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Pursell <dpursell@google.com>2024-01-16 17:54:47 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2024-01-16 17:54:47 +0000
commitbd602078f5ffc3141b7d4711cb0ee29fcaabc6d0 (patch)
tree720f4ee1a0b8c91d5ec5585ccc4c96b8d8457050
parent8a0c7a23e6fec34f9a4a178f0207db8ffdc6bfbe (diff)
parented3b0fb71efbd78a354324167d62b32645421aee (diff)
downloadavb-bd602078f5ffc3141b7d4711cb0ee29fcaabc6d0.tar.gz
libavb_rs: add property descriptor support am: 7cee66d62d am: 8510ca5de8 am: ed3b0fb71e
Original change: https://android-review.googlesource.com/c/platform/external/avb/+/2908979 Change-Id: I4779c55677a585a7246cd38e0fb7b32f05be853b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--rust/Android.bp13
-rw-r--r--rust/src/descriptor/hash.rs51
-rw-r--r--rust/src/descriptor/mod.rs10
-rw-r--r--rust/src/descriptor/property.rs112
-rw-r--r--rust/src/descriptor/util.rs110
-rw-r--r--rust/src/lib.rs1
-rw-r--r--rust/tests/verify_tests.rs33
7 files changed, 298 insertions, 32 deletions
diff --git a/rust/Android.bp b/rust/Android.bp
index 7ef3df3..cafc675 100644
--- a/rust/Android.bp
+++ b/rust/Android.bp
@@ -264,6 +264,7 @@ rust_defaults {
":avbrs_test_vbmeta",
":avbrs_test_vbmeta_2_parts",
":avbrs_test_vbmeta_persistent_digest",
+ ":avbrs_test_vbmeta_with_property",
],
rustlibs: ["libhex"],
test_suites: ["general-tests"],
@@ -364,6 +365,18 @@ genrule {
cmd: "$(location avbtool) make_vbmeta_image --key $(location :avb_testkey_rsa4096) --algorithm SHA512_RSA4096 --include_descriptors_from_image $(location :avbrs_test_image_descriptor_persistent_digest) --output $(out)",
}
+// Standalone vbmeta image with property descriptor "test_prop_key" = "test_prop_value".
+genrule {
+ name: "avbrs_test_vbmeta_with_property",
+ tools: ["avbtool"],
+ srcs: [
+ ":avbrs_test_image_descriptor",
+ ":avb_testkey_rsa4096",
+ ],
+ out: ["test_vbmeta_with_property.img"],
+ cmd: "$(location avbtool) make_vbmeta_image --prop test_prop_key:test_prop_value --key $(location :avb_testkey_rsa4096) --algorithm SHA512_RSA4096 --include_descriptors_from_image $(location :avbrs_test_image_descriptor) --output $(out)",
+}
+
// Combined test image + signed vbmeta footer for "test_part".
avb_add_hash_footer {
name: "avbrs_test_image_with_vbmeta_footer",
diff --git a/rust/src/descriptor/hash.rs b/rust/src/descriptor/hash.rs
index edc49ec..3ad52f0 100644
--- a/rust/src/descriptor/hash.rs
+++ b/rust/src/descriptor/hash.rs
@@ -14,10 +14,12 @@
//! Hash descriptors.
-use super::{util::split_slice, DescriptorError, DescriptorResult};
+use super::{
+ util::{parse_descriptor, split_slice, ValidateAndByteswap, ValidationFunc},
+ DescriptorError, DescriptorResult,
+};
use avb_bindgen::{avb_hash_descriptor_validate_and_byteswap, AvbHashDescriptor};
use core::{ffi::CStr, str::from_utf8};
-use zerocopy::Ref;
/// `AvbHashDescriptorFlags`; see libavb docs for details.
pub use avb_bindgen::AvbHashDescriptorFlags as HashDescriptorFlags;
@@ -50,6 +52,12 @@ pub struct HashDescriptor<'a> {
pub digest: &'a [u8],
}
+// SAFETY: `VALIDATE_AND_BYTESWAP_FUNC` is the correct libavb validator for this descriptor type.
+unsafe impl ValidateAndByteswap for AvbHashDescriptor {
+ const VALIDATE_AND_BYTESWAP_FUNC: ValidationFunc<Self> =
+ avb_hash_descriptor_validate_and_byteswap;
+}
+
impl<'a> HashDescriptor<'a> {
/// Extract a `HashDescriptor` from the given descriptor contents.
///
@@ -60,41 +68,26 @@ impl<'a> HashDescriptor<'a> {
/// The new descriptor, or `DescriptorError` if the given `contents` aren't a valid
/// `AvbHashDescriptor`.
pub(super) fn new(contents: &'a [u8]) -> DescriptorResult<Self> {
- let (raw_header, remainder) = Ref::<_, AvbHashDescriptor>::new_from_prefix(contents)
- .ok_or(DescriptorError::InvalidHeader)?;
- let raw_header = raw_header.into_ref();
-
- let header = {
- let mut header = AvbHashDescriptor::default();
- // SAFETY:
- // * `avb_hash_descriptor_validate_and_byteswap()` checks the validity of the fields
- // * even if `raw_header` is corrupted somehow, this will only give bogus header values
- // as output which will be caught below; it will never try to access memory outside
- // of the header.
- if !unsafe { avb_hash_descriptor_validate_and_byteswap(raw_header, &mut header) } {
- return Err(DescriptorError::InvalidHeader);
- }
- header
- };
-
- // Descriptor body contains: name + salt + digest.
- let (partition_name, remainder) = split_slice(remainder, header.partition_name_len)?;
- let (salt, remainder) = split_slice(remainder, header.salt_len)?;
- let (digest, _) = split_slice(remainder, header.digest_len)?;
-
- // Extract the hash algorithm from the original contents since the temporary `header`
- // doesn't live past this function.
+ // Descriptor contains: header + name + salt + digest.
+ let descriptor = parse_descriptor::<AvbHashDescriptor>(contents)?;
+ let (partition_name, remainder) =
+ split_slice(descriptor.body, descriptor.header.partition_name_len)?;
+ let (salt, remainder) = split_slice(remainder, descriptor.header.salt_len)?;
+ let (digest, _) = split_slice(remainder, descriptor.header.digest_len)?;
+
+ // Extract the hash algorithm from the original raw header since the temporary
+ // byte-swapped header doesn't live past this function.
// The hash algorithm is a nul-terminated UTF-8 string which is identical in the raw
// and byteswapped headers.
- let hash_algorithm = CStr::from_bytes_until_nul(&raw_header.hash_algorithm)
+ let hash_algorithm = CStr::from_bytes_until_nul(&descriptor.raw_header.hash_algorithm)
.map_err(|_| DescriptorError::InvalidValue)?
.to_str()
.map_err(|_| DescriptorError::InvalidUtf8)?;
Ok(Self {
- image_size: header.image_size,
+ image_size: descriptor.header.image_size,
hash_algorithm,
- flags: HashDescriptorFlags(header.flags),
+ flags: HashDescriptorFlags(descriptor.header.flags),
partition_name: from_utf8(partition_name).map_err(|_| DescriptorError::InvalidUtf8)?,
salt,
digest,
diff --git a/rust/src/descriptor/mod.rs b/rust/src/descriptor/mod.rs
index fdc4956..ba741dd 100644
--- a/rust/src/descriptor/mod.rs
+++ b/rust/src/descriptor/mod.rs
@@ -20,6 +20,7 @@
extern crate alloc;
mod hash;
+mod property;
mod util;
use crate::VbmetaData;
@@ -30,13 +31,14 @@ use avb_bindgen::{
use core::{ffi::c_void, mem::size_of, slice};
pub use hash::{HashDescriptor, HashDescriptorFlags};
+pub use property::PropertyDescriptor;
/// A single descriptor.
// TODO(b/290110273): add support for full descriptor contents.
#[derive(Debug)]
pub enum Descriptor<'a> {
/// Wraps `AvbPropertyDescriptor`.
- Property(&'a [u8]),
+ Property(PropertyDescriptor<'a>),
/// Wraps `AvbHashtreeDescriptor`.
Hashtree(&'a [u8]),
/// Wraps `AvbHashDescriptor`.
@@ -60,6 +62,8 @@ pub enum DescriptorError {
InvalidSize,
/// A field that was supposed to be valid UTF-8 was not.
InvalidUtf8,
+ /// Descriptor contents don't match what we expect.
+ InvalidContents,
}
/// `Result` type for `DescriptorError` errors.
@@ -103,7 +107,9 @@ impl<'a> Descriptor<'a> {
let contents = unsafe { slice::from_raw_parts(raw_descriptor as *const u8, total_size) };
match descriptor.tag.try_into() {
- Ok(AvbDescriptorTag::AVB_DESCRIPTOR_TAG_PROPERTY) => Ok(Descriptor::Property(contents)),
+ Ok(AvbDescriptorTag::AVB_DESCRIPTOR_TAG_PROPERTY) => {
+ Ok(Descriptor::Property(PropertyDescriptor::new(contents)?))
+ }
Ok(AvbDescriptorTag::AVB_DESCRIPTOR_TAG_HASHTREE) => Ok(Descriptor::Hashtree(contents)),
Ok(AvbDescriptorTag::AVB_DESCRIPTOR_TAG_HASH) => {
Ok(Descriptor::Hash(HashDescriptor::new(contents)?))
diff --git a/rust/src/descriptor/property.rs b/rust/src/descriptor/property.rs
new file mode 100644
index 0000000..62c3be0
--- /dev/null
+++ b/rust/src/descriptor/property.rs
@@ -0,0 +1,112 @@
+// Copyright 2023, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Property descriptors.
+
+use super::{
+ util::{parse_descriptor, split_slice, ValidateAndByteswap, ValidationFunc},
+ DescriptorError, DescriptorResult,
+};
+use avb_bindgen::{avb_property_descriptor_validate_and_byteswap, AvbPropertyDescriptor};
+use core::str::from_utf8;
+
+/// Checks that the first byte is nul and discards it.
+/// Returns the remainder of `bytes` on success, or `DescriptorError` if the byte wasn't nul.
+fn extract_nul(bytes: &[u8]) -> DescriptorResult<&[u8]> {
+ let (nul, remainder) = split_slice(bytes, 1)?;
+ match nul {
+ b"\0" => Ok(remainder),
+ _ => Err(DescriptorError::InvalidContents),
+ }
+}
+
+/// Wraps an `AvbPropertyDescriptor` stored in a vbmeta image.
+#[derive(Debug, PartialEq, Eq)]
+pub struct PropertyDescriptor<'a> {
+ /// Key is always UTF-8.
+ pub key: &'a str,
+
+ /// Value can be arbitrary bytes.
+ pub value: &'a [u8],
+}
+
+// SAFETY: `VALIDATE_AND_BYTESWAP_FUNC` is the correct libavb validator for this descriptor type.
+unsafe impl ValidateAndByteswap for AvbPropertyDescriptor {
+ const VALIDATE_AND_BYTESWAP_FUNC: ValidationFunc<Self> =
+ avb_property_descriptor_validate_and_byteswap;
+}
+
+impl<'a> PropertyDescriptor<'a> {
+ /// Extract a `PropertyDescriptor` from the given descriptor contents.
+ ///
+ /// # Arguments
+ /// * `contents`: descriptor contents, including the header, in raw big-endian format.
+ ///
+ /// # Returns
+ /// The new descriptor, or `DescriptorError` if the given `contents` aren't a valid
+ /// `AvbPropertyDescriptor`.
+ pub(super) fn new(contents: &'a [u8]) -> DescriptorResult<Self> {
+ // Descriptor contains: header + key + nul + value + nul.
+ let descriptor = parse_descriptor::<AvbPropertyDescriptor>(contents)?;
+ let (key, remainder) = split_slice(descriptor.body, descriptor.header.key_num_bytes)?;
+ let remainder = extract_nul(remainder)?;
+ let (value, remainder) = split_slice(remainder, descriptor.header.value_num_bytes)?;
+ extract_nul(remainder)?;
+
+ Ok(Self {
+ key: from_utf8(key).map_err(|_| DescriptorError::InvalidUtf8)?,
+ value,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::mem::size_of;
+
+ /// A valid property descriptor in raw big-endian format.
+ const TEST_PROPERTY_DESCRIPTOR: &[u8] = &[
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x5F, 0x70, 0x72, 0x6F, 0x70, 0x5F, 0x6B, 0x65, 0x79,
+ 0x00, 0x74, 0x65, 0x73, 0x74, 0x5F, 0x70, 0x72, 0x6F, 0x70, 0x5F, 0x76, 0x61, 0x6C, 0x75,
+ 0x65, 0x00, 0x00, 0x00,
+ ];
+
+ #[test]
+ fn new_property_descriptor_success() {
+ let descriptor = PropertyDescriptor::new(TEST_PROPERTY_DESCRIPTOR);
+ assert!(descriptor.is_ok());
+ }
+
+ #[test]
+ fn new_property_descriptor_too_short_header_fails() {
+ let bad_header_size = size_of::<AvbPropertyDescriptor>() - 1;
+ assert_eq!(
+ PropertyDescriptor::new(&TEST_PROPERTY_DESCRIPTOR[..bad_header_size]).unwrap_err(),
+ DescriptorError::InvalidHeader
+ );
+ }
+
+ #[test]
+ fn new_property_descriptor_too_short_contents_fails() {
+ // The last 2 bytes are padding, so we need to drop 3 bytes to trigger an error.
+ let bad_contents_size = TEST_PROPERTY_DESCRIPTOR.len() - 3;
+ assert_eq!(
+ PropertyDescriptor::new(&TEST_PROPERTY_DESCRIPTOR[..bad_contents_size]).unwrap_err(),
+ DescriptorError::InvalidSize
+ );
+ }
+}
diff --git a/rust/src/descriptor/util.rs b/rust/src/descriptor/util.rs
index ada80fc..e4e23fd 100644
--- a/rust/src/descriptor/util.rs
+++ b/rust/src/descriptor/util.rs
@@ -15,6 +15,7 @@
//! Descriptor utilities.
use super::{DescriptorError, DescriptorResult};
+use zerocopy::{FromBytes, FromZeroes, Ref};
/// Splits `size` bytes off the front of `data`.
///
@@ -41,9 +42,73 @@ where
}
}
+/// Function type for the `avb_*descriptor_validate_and_byteswap()` C functions.
+pub(super) type ValidationFunc<T> = unsafe extern "C" fn(*const T, *mut T) -> bool;
+
+/// Trait to represent an `Avb*Descriptor` type which has an associated `ValidationFunc`.
+///
+/// This allows the generic `parse_descriptor()` function to extract a descriptor header for any
+/// descriptor type.
+///
+/// To use, implement `ValidateAndByteSwap` on the `Avb*Descriptor` struct.
+///
+/// # Safety
+/// The function assigned to `VALIDATE_AND_BYTESWAP_FUNC` must be the libavb
+/// `avb_*descriptor_validate_and_byteswap()` function corresponding to the descriptor implementing
+/// this trait (e.g. `AvbHashDescriptor` -> `avb_hash_descriptor_validate_and_byteswap`).
+pub(super) unsafe trait ValidateAndByteswap {
+ /// The specific libavb validation function for this descriptor type.
+ const VALIDATE_AND_BYTESWAP_FUNC: ValidationFunc<Self>;
+}
+
+/// A descriptor that has been extracted, validated, and byteswapped.
+#[derive(Debug)]
+pub(super) struct ParsedDescriptor<'a, T> {
+ /// The original raw (big-endian) header.
+ pub raw_header: &'a T,
+ /// A copy of the header in host-endian format.
+ pub header: T,
+ /// The descriptor body contents.
+ pub body: &'a [u8],
+}
+
+/// Extracts the descriptor header from the given buffer.
+///
+/// # Arguments
+/// `data`: the descriptor contents in raw (big-endian) format.
+///
+/// # Returns
+/// A `ParsedDescriptor` on success, `DescriptorError` if `data` was too small or the header looks
+/// invalid.
+pub(super) fn parse_descriptor<T>(data: &[u8]) -> DescriptorResult<ParsedDescriptor<T>>
+where
+ T: Default + FromZeroes + FromBytes + ValidateAndByteswap,
+{
+ let (raw_header, body) =
+ Ref::<_, T>::new_from_prefix(data).ok_or(DescriptorError::InvalidHeader)?;
+ let raw_header = raw_header.into_ref();
+
+ let mut header = T::default();
+ // SAFETY:
+ // * all `VALIDATE_AND_BYTESWAP_FUNC` functions check the validity of the fields.
+ // * even if the data is corrupted somehow and is not detected by the validation logic, these
+ // functions never try to access memory outside of the header.
+ if !unsafe { T::VALIDATE_AND_BYTESWAP_FUNC(raw_header, &mut header) } {
+ return Err(DescriptorError::InvalidHeader);
+ }
+
+ Ok(ParsedDescriptor {
+ raw_header,
+ header,
+ body,
+ })
+}
+
#[cfg(test)]
mod tests {
use super::*;
+ use avb_bindgen::{avb_descriptor_validate_and_byteswap, AvbDescriptor, AvbHashDescriptor};
+ use std::mem::size_of;
#[test]
fn split_slice_with_various_size_types_succeeds() {
@@ -65,4 +130,49 @@ mod tests {
let data = &[1, 2, 3, 4];
assert_eq!(split_slice(data, 5u32), Err(DescriptorError::InvalidSize));
}
+
+ // Enable `parse_descriptor()` on a generic `AvbDescriptor` of any sub-type.
+ // SAFETY: `VALIDATE_AND_BYTESWAP_FUNC` is the correct libavb validator for this descriptor.
+ unsafe impl ValidateAndByteswap for AvbDescriptor {
+ const VALIDATE_AND_BYTESWAP_FUNC: ValidationFunc<Self> =
+ avb_descriptor_validate_and_byteswap;
+ }
+
+ // Hardcoded test descriptor of custom sub-type (tag = 42).
+ const TEST_DESCRIPTOR: &[u8] = &[
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, // tag = 0x42u64 (BE)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // num_bytes_following = 8u64 (BE)
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // fake contents
+ ];
+
+ #[test]
+ fn parse_descriptor_success() {
+ let descriptor = parse_descriptor::<AvbDescriptor>(TEST_DESCRIPTOR).unwrap();
+
+ // `assert_eq!()` cannot be used on `repr(packed)` struct fields.
+ assert!(descriptor.raw_header.tag == 0x42u64.to_be());
+ assert!(descriptor.raw_header.num_bytes_following == 8u64.to_be());
+ assert!(descriptor.header.tag == 0x42);
+ assert!(descriptor.header.num_bytes_following == 8);
+ assert_eq!(descriptor.body, &[1, 2, 3, 4, 5, 6, 7, 8]);
+ }
+
+ #[test]
+ fn parse_descriptor_buffer_too_short_failure() {
+ let bad_length = size_of::<AvbDescriptor>() - 1;
+ assert_eq!(
+ parse_descriptor::<AvbDescriptor>(&TEST_DESCRIPTOR[..bad_length]).unwrap_err(),
+ DescriptorError::InvalidHeader
+ );
+ }
+
+ #[test]
+ fn parse_descriptor_wrong_type_failure() {
+ assert_eq!(
+ // `TEST_DESCRIPTOR` is not a valid `AvbHashDescriptor`, this should fail without
+ // triggering any undefined behavior.
+ parse_descriptor::<AvbHashDescriptor>(TEST_DESCRIPTOR).unwrap_err(),
+ DescriptorError::InvalidHeader
+ );
+ }
}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 8da12fd..a5419f0 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -33,6 +33,7 @@ mod verify;
pub use descriptor::{
Descriptor, DescriptorError, DescriptorResult, HashDescriptor, HashDescriptorFlags,
+ PropertyDescriptor,
};
pub use error::{
IoError, IoResult, SlotVerifyError, SlotVerifyNoDataResult, SlotVerifyResult,
diff --git a/rust/tests/verify_tests.rs b/rust/tests/verify_tests.rs
index 36a4647..61b0576 100644
--- a/rust/tests/verify_tests.rs
+++ b/rust/tests/verify_tests.rs
@@ -17,7 +17,7 @@
use crate::test_ops::TestOps;
use avb::{
slot_verify, Descriptor, HashDescriptor, HashDescriptorFlags, HashtreeErrorMode, IoError,
- SlotVerifyData, SlotVerifyError, SlotVerifyFlags, SlotVerifyResult,
+ PropertyDescriptor, SlotVerifyData, SlotVerifyError, SlotVerifyFlags, SlotVerifyResult,
};
use hex::decode;
use std::{ffi::CString, fs};
@@ -35,6 +35,7 @@ const TEST_IMAGE_HASH_ALGO: &str = "sha256"; // Default value, we don't explicit
const TEST_VBMETA_PATH: &str = "test_vbmeta.img";
const TEST_VBMETA_2_PARTITIONS_PATH: &str = "test_vbmeta_2_parts.img";
const TEST_VBMETA_PERSISTENT_DIGEST_PATH: &str = "test_vbmeta_persistent_digest.img";
+const TEST_VBMETA_WITH_PROPERTY_PATH: &str = "test_vbmeta_with_property.img";
const TEST_IMAGE_WITH_VBMETA_FOOTER_PATH: &str = "avbrs_test_image_with_vbmeta_footer.img";
const TEST_IMAGE_WITH_VBMETA_FOOTER_FOR_BOOT_PATH: &str =
"avbrs_test_image_with_vbmeta_footer_for_boot.img";
@@ -44,6 +45,8 @@ const TEST_PARTITION_SLOT_C_NAME: &str = "test_part_c";
const TEST_PARTITION_2_NAME: &str = "test_part_2";
const TEST_PARTITION_PERSISTENT_DIGEST_NAME: &str = "test_part_persistent_digest";
const TEST_VBMETA_ROLLBACK_LOCATION: usize = 0; // Default value, we don't explicitly set this.
+const TEST_PROPERTY_KEY: &str = "test_prop_key";
+const TEST_PROPERTY_VALUE: &[u8] = b"test_prop_value";
/// Initializes a `TestOps` object such that verification will succeed on `TEST_PARTITION_NAME`.
fn test_ops_one_image_one_vbmeta() -> TestOps {
@@ -654,3 +657,31 @@ fn verify_hash_descriptor() {
}
)
}
+
+#[test]
+fn verify_property_descriptor() {
+ let mut ops = test_ops_one_image_one_vbmeta();
+ // Replace vbmeta with the version containing a property descriptor.
+ ops.add_partition("vbmeta", fs::read(TEST_VBMETA_WITH_PROPERTY_PATH).unwrap());
+
+ let result = verify_one_image_one_vbmeta(&mut ops);
+
+ let data = result.unwrap();
+ let descriptors = &data.vbmeta_data()[0].descriptors().unwrap();
+ let descriptor = descriptors
+ .iter()
+ .filter_map(|d| match d {
+ Descriptor::Property(p) => Some(p),
+ _ => None,
+ })
+ .next()
+ .unwrap();
+
+ assert_eq!(
+ descriptor,
+ &PropertyDescriptor {
+ key: TEST_PROPERTY_KEY,
+ value: TEST_PROPERTY_VALUE
+ }
+ )
+}