diff options
author | David Pursell <dpursell@google.com> | 2024-04-09 16:38:06 -0700 |
---|---|---|
committer | David Pursell <dpursell@google.com> | 2024-04-26 13:39:29 -0700 |
commit | f9d4620d48b2a2b7310bbb56c0c8af821964c5f1 (patch) | |
tree | b6725d91c88dd8c52e9efa011d743a62d1c093dd | |
parent | 26eaa0afb46fd0ff56287a8e568fea36ec673f53 (diff) | |
download | avb-f9d4620d48b2a2b7310bbb56c0c8af821964c5f1.tar.gz |
Implement `cert_validate_unlock_credential()`.
Bug: b/320543206
Test: atest libavb_rs_test libavb_rs_uuid_test libavb_rs_unittest libavb_rs_uuid_unittest
Change-Id: If2e44d40f88081f43bf97b799c25b32ebf77230a
-rw-r--r-- | rust/Android.bp | 8 | ||||
-rw-r--r-- | rust/src/cert.rs | 34 | ||||
-rw-r--r-- | rust/src/lib.rs | 4 | ||||
-rw-r--r-- | rust/tests/cert_tests.rs | 104 | ||||
-rw-r--r-- | rust/tests/test_data.rs | 2 |
5 files changed, 138 insertions, 14 deletions
diff --git a/rust/Android.bp b/rust/Android.bp index 756813d..9188819 100644 --- a/rust/Android.bp +++ b/rust/Android.bp @@ -35,14 +35,16 @@ rust_defaults { "--default-enum-style rust", "--with-derive-default", "--with-derive-custom=Avb.*Descriptor=FromZeroes,FromBytes", - "--with-derive-custom=AvbCertPermanentAttributes=FromZeroes,FromBytes", + "--with-derive-custom=AvbCertPermanentAttributes=FromZeroes,FromBytes,AsBytes", + "--with-derive-custom=AvbCertCertificate.*=FromZeroes,FromBytes,AsBytes", + "--with-derive-custom=AvbCertUnlock.*=FromZeroes,FromBytes,AsBytes", "--allowlist-type=AvbDescriptorTag", "--allowlist-type=Avb.*Flags", "--allowlist-function=.*", "--allowlist-var=AVB.*", "--use-core", "--raw-line=#![no_std]", - "--raw-line=use zerocopy::{FromZeroes, FromBytes};", + "--raw-line=use zerocopy::{AsBytes, FromBytes, FromZeroes};", "--ctypes-prefix=core::ffi", ], cflags: ["-DBORINGSSL_NO_CXX"], @@ -292,6 +294,8 @@ rust_defaults { srcs: ["tests/tests.rs"], data: [ ":avb_cert_test_permanent_attributes", + ":avb_cert_test_unlock_challenge", + ":avb_cert_test_unlock_credential", ":avb_testkey_rsa4096_pub_bin", ":avb_testkey_rsa8192_pub_bin", ":avbrs_test_image", diff --git a/rust/src/cert.rs b/rust/src/cert.rs index 2d673ad..aa54859 100644 --- a/rust/src/cert.rs +++ b/rust/src/cert.rs @@ -84,7 +84,10 @@ //! ``` use crate::{error::io_enum_to_result, ops, IoError, IoResult, Ops, PublicKeyForPartitionInfo}; -use avb_bindgen::{avb_cert_generate_unlock_challenge, avb_cert_validate_vbmeta_public_key}; +use avb_bindgen::{ + avb_cert_generate_unlock_challenge, avb_cert_validate_unlock_credential, + avb_cert_validate_vbmeta_public_key, +}; use core::{ffi::CStr, pin::pin}; #[cfg(feature = "uuid")] use uuid::Uuid; @@ -288,16 +291,37 @@ pub fn cert_generate_unlock_challenge(cert_ops: &mut dyn CertOps) -> IoResult<Ce /// # Returns /// * `Ok(true)` if the credential validated /// * `Ok(false)` if it failed validation +/// * `Err(IoError::NotImplemented)` if `ops` does not provide the required `cert_ops()`. /// * `Err(IoError)` on `ops` failure pub fn cert_validate_unlock_credential( // Note: in the libavb C API this function takes an `AvbCertOps` rather than `AvbOps`, but // the implementation requires both, so we need an `Ops` here. This is also more consistent // with `validate_vbmeta_public_key()` which similarly requires both but takes `AvbOps`. - _ops: &mut dyn Ops, - _credential: &CertUnlockCredential, + ops: &mut dyn Ops, + credential: &CertUnlockCredential, ) -> IoResult<bool> { - // TODO(b/320543206): implement - Err(IoError::NotImplemented) + // This API requires both AVB and cert ops. + if ops.cert_ops().is_none() { + return Err(IoError::NotImplemented); + } + + let ops_bridge = pin!(ops::OpsBridge::new(ops)); + let mut trusted = false; + io_enum_to_result( + // SAFETY: + // * `ops_bridge.init_and_get_c_ops()` gives us a valid `AvbOps` with cert. + // * `credential` is a valid C-compatible `CertUnlockCredential`. + // * `trusted` is a C-compatible bool. + // * this function does not retain references to any of these arguments. + unsafe { + avb_cert_validate_unlock_credential( + ops_bridge.init_and_get_c_ops().cert_ops, + credential, + &mut trusted, + ) + }, + )?; + Ok(trusted) } /// An `Ops` implementation that only provides the `cert_ops()` callback. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index cb8b6d3..99962ab 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -34,8 +34,8 @@ mod verify; pub use cert::{ cert_generate_unlock_challenge, cert_validate_unlock_credential, - cert_validate_vbmeta_public_key, CertOps, CertPermanentAttributes, CERT_PIK_VERSION_LOCATION, - CERT_PSK_VERSION_LOCATION, SHA256_DIGEST_SIZE, + cert_validate_vbmeta_public_key, CertOps, CertPermanentAttributes, CertUnlockChallenge, + CertUnlockCredential, CERT_PIK_VERSION_LOCATION, CERT_PSK_VERSION_LOCATION, SHA256_DIGEST_SIZE, }; pub use descriptor::{ ChainPartitionDescriptor, ChainPartitionDescriptorFlags, Descriptor, DescriptorError, diff --git a/rust/tests/cert_tests.rs b/rust/tests/cert_tests.rs index 5921b7a..77cd048 100644 --- a/rust/tests/cert_tests.rs +++ b/rust/tests/cert_tests.rs @@ -21,14 +21,20 @@ use crate::{ verify_one_image_one_vbmeta, }; use avb::{ - cert_generate_unlock_challenge, CertPermanentAttributes, IoError, SlotVerifyError, - CERT_PIK_VERSION_LOCATION, CERT_PSK_VERSION_LOCATION, + cert_generate_unlock_challenge, cert_validate_unlock_credential, CertPermanentAttributes, + CertUnlockChallenge, CertUnlockCredential, IoError, SlotVerifyError, CERT_PIK_VERSION_LOCATION, + CERT_PSK_VERSION_LOCATION, }; use hex::decode; -use std::{collections::HashMap, fs}; -use zerocopy::FromBytes; +use std::{collections::HashMap, fs, mem::size_of}; +use zerocopy::{AsBytes, FromBytes}; -/// Initializes a `TestOps` object such that cert verification will succeed on `TEST_PARTITION_NAME`. +/// Initializes a `TestOps` object such that cert verification will succeed on +/// `TEST_PARTITION_NAME`. +/// +/// The returned `TestOps` also contains RNG configured to return the contents of +/// `TEST_CERT_UNLOCK_CHALLENGE_RNG_PATH`, so that the pre-signed contents of +/// `TEST_CERT_UNLOCK_CREDENTIAL_PATH` will successfully validate by default. fn build_test_cert_ops_one_image_one_vbmeta<'a>() -> TestOps<'a> { let mut ops = build_test_ops_one_image_one_vbmeta(); @@ -56,9 +62,19 @@ fn build_test_cert_ops_one_image_one_vbmeta<'a>() -> TestOps<'a> { ops.rollbacks .insert(CERT_PSK_VERSION_LOCATION, TEST_CERT_PSK_VERSION); + // It's non-trivial to sign a challenge without `avbtool.py`, so instead we inject the exact RNG + // used by the pre-generated challenge so that we can use the pre-signed credential. + ops.cert_fake_rng = fs::read(TEST_CERT_UNLOCK_CHALLENGE_RNG_PATH).unwrap(); + ops } +/// Returns the contents of `TEST_CERT_UNLOCK_CREDENTIAL_PATH` as a `CertUnlockCredential`. +fn test_unlock_credential() -> CertUnlockCredential { + let credential_bytes = fs::read(TEST_CERT_UNLOCK_CREDENTIAL_PATH).unwrap(); + CertUnlockCredential::read_from(&credential_bytes[..]).unwrap() +} + /// Enough fake RNG data to generate a single unlock challenge. const UNLOCK_CHALLENGE_FAKE_RNG: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; @@ -193,3 +209,81 @@ fn cert_generate_unlock_challenge_fails_insufficient_rng() { IoError::Io ); } + +#[test] +fn cert_validate_unlock_credential_success() { + let mut ops = build_test_cert_ops_one_image_one_vbmeta(); + + // We don't actually need the challenge here since we've pre-signed it, but we still need to + // call this function so the libavb_cert internal state is ready for the unlock cred. + let _ = cert_generate_unlock_challenge(&mut ops).unwrap(); + + assert_eq!( + cert_validate_unlock_credential(&mut ops, &test_unlock_credential()), + Ok(true) + ); +} + +#[test] +fn cert_validate_unlock_credential_fails_wrong_rng() { + let mut ops = build_test_cert_ops_one_image_one_vbmeta(); + // Modify the RNG slightly, the cerificate should now fail to validate. + ops.cert_fake_rng[0] ^= 0x01; + + let _ = cert_generate_unlock_challenge(&mut ops).unwrap(); + + assert_eq!( + cert_validate_unlock_credential(&mut ops, &test_unlock_credential()), + Ok(false) + ); +} + +#[test] +fn cert_validate_unlock_credential_fails_with_pik_rollback_violation() { + let mut ops = build_test_cert_ops_one_image_one_vbmeta(); + // Rotating the PIK should invalidate all existing unlock keys, which includes our pre-signed + // certificate. + *ops.rollbacks.get_mut(&CERT_PIK_VERSION_LOCATION).unwrap() += 1; + + let _ = cert_generate_unlock_challenge(&mut ops).unwrap(); + + assert_eq!( + cert_validate_unlock_credential(&mut ops, &test_unlock_credential()), + Ok(false) + ); +} + +#[test] +fn cert_validate_unlock_credential_fails_no_challenge() { + let mut ops = build_test_cert_ops_one_image_one_vbmeta(); + + // We never called `cert_generate_unlock_challenge()`, so no credentials should validate. + assert_eq!( + cert_validate_unlock_credential(&mut ops, &test_unlock_credential()), + Ok(false) + ); +} + +// In practice, devices will usually be passing unlock challenges and credentials over fastboot as +// raw bytes. This test ensures that there are some reasonable APIs available to convert between +// `CertUnlockChallenge`/`CertUnlockCredential` and byte slices. +#[test] +fn cert_validate_unlock_credential_bytes_api() { + let mut ops = build_test_cert_ops_one_image_one_vbmeta(); + + // Write an unlock challenge to a byte buffer for TX over fastboot. + let challenge = cert_generate_unlock_challenge(&mut ops).unwrap(); + let mut buffer = vec![0u8; size_of::<CertUnlockChallenge>()]; + assert_eq!(challenge.write_to(&mut buffer[..]), Some(())); // zerocopy::AsBytes. + + // Read an unlock credential from a byte buffer for RX from fastboot. + let buffer = vec![0u8; size_of::<CertUnlockCredential>()]; + let credential = CertUnlockCredential::ref_from(&buffer[..]).unwrap(); // zerocopy::FromBytes. + + // It shouldn't actually validate since the credential is just zeroes, the important thing + // is that it compiles. + assert_eq!( + cert_validate_unlock_credential(&mut ops, credential), + Ok(false) + ); +} diff --git a/rust/tests/test_data.rs b/rust/tests/test_data.rs index 3db13d5..4912bd6 100644 --- a/rust/tests/test_data.rs +++ b/rust/tests/test_data.rs @@ -57,6 +57,8 @@ pub const TEST_HASHTREE_ALGORITHM: &str = "sha1"; // Certificate test data. pub const TEST_CERT_PERMANENT_ATTRIBUTES_PATH: &str = "data/cert_permanent_attributes.bin"; pub const TEST_CERT_VBMETA_PATH: &str = "test_vbmeta_cert.img"; +pub const TEST_CERT_UNLOCK_CHALLENGE_RNG_PATH: &str = "data/cert_unlock_challenge.bin"; +pub const TEST_CERT_UNLOCK_CREDENTIAL_PATH: &str = "data/cert_unlock_credential.bin"; // The cert test keys were both generated with rollback version 42. pub const TEST_CERT_PIK_VERSION: u64 = 42; |