libavb_rs: unlock credential validation

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
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 @@
         "--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 @@
     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 @@
 /// # 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 @@
 
 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 @@
     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 @@
     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 @@
         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 @@
 // 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;