Merge changes from topic "ag-wire" into main am: 0a44da8589

Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/2817768

Change-Id: I61ee72e3ec5320717c09d8fba01ec4a78f1ba18a
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/security/authgraph/aidl/vts/functional/Android.bp b/security/authgraph/aidl/vts/functional/Android.bp
index fc13759..0e3480f 100644
--- a/security/authgraph/aidl/vts/functional/Android.bp
+++ b/security/authgraph/aidl/vts/functional/Android.bp
@@ -46,3 +46,36 @@
         "vts",
     ],
 }
+
+rust_test {
+    name: "VtsAidlAuthGraphRoleTest",
+    srcs: ["role_test.rs"],
+    test_suites: [
+        "general-tests",
+        "vts",
+    ],
+    defaults: [
+        "authgraph_use_latest_hal_aidl_rust",
+    ],
+    rustlibs: [
+        "libauthgraph_vts_test",
+        "libbinder_rs",
+    ],
+}
+
+rust_library {
+    name: "libauthgraph_vts_test",
+    crate_name: "authgraph_vts_test",
+    srcs: ["lib.rs"],
+    defaults: [
+        "authgraph_use_latest_hal_aidl_rust",
+    ],
+    rustlibs: [
+        "libauthgraph_boringssl",
+        "libauthgraph_core",
+        "libauthgraph_hal",
+        "libauthgraph_nonsecure",
+        "libbinder_rs",
+        "libcoset",
+    ],
+}
diff --git a/security/authgraph/aidl/vts/functional/lib.rs b/security/authgraph/aidl/vts/functional/lib.rs
new file mode 100644
index 0000000..7b9b2b9
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/lib.rs
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! VTS test library for AuthGraph functionality.
+//!
+//! This test code is bundled as a library, not as `[cfg(test)]`, to allow it to be
+//! re-used inside the (Rust) VTS tests of components that use AuthGraph.
+
+use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+    Error::Error, IAuthGraphKeyExchange::IAuthGraphKeyExchange, Identity::Identity,
+    PlainPubKey::PlainPubKey, PubKey::PubKey, SessionIdSignature::SessionIdSignature,
+};
+use authgraph_boringssl as boring;
+use authgraph_core::keyexchange as ke;
+use authgraph_core::{arc, key, traits};
+use authgraph_nonsecure::StdClock;
+use coset::CborSerializable;
+
+pub mod sink;
+pub mod source;
+
+/// Return a collection of AuthGraph trait implementations suitable for testing.
+pub fn test_impls() -> traits::TraitImpl {
+    // Note that the local implementation is using a clock with a potentially different epoch than
+    // the implementation under test.
+    boring::trait_impls(
+        Box::<boring::test_device::AgDevice>::default(),
+        Some(Box::new(StdClock::default())),
+    )
+}
+
+fn build_plain_pub_key(pub_key: &Option<Vec<u8>>) -> PubKey {
+    PubKey::PlainKey(PlainPubKey {
+        plainPubKey: pub_key.clone().unwrap(),
+    })
+}
+
+fn extract_plain_pub_key(pub_key: &Option<PubKey>) -> &PlainPubKey {
+    match pub_key {
+        Some(PubKey::PlainKey(pub_key)) => pub_key,
+        Some(PubKey::SignedKey(_)) => panic!("expect unsigned public key"),
+        None => panic!("expect pubKey to be populated"),
+    }
+}
+
+fn verification_key_from_identity(impls: &traits::TraitImpl, identity: &[u8]) -> key::EcVerifyKey {
+    let identity = key::Identity::from_slice(identity).expect("invalid identity CBOR");
+    impls
+        .device
+        .process_peer_cert_chain(&identity.cert_chain, &*impls.ecdsa)
+        .expect("failed to extract signing key")
+}
+
+fn vec_to_identity(data: &[u8]) -> Identity {
+    Identity {
+        identity: data.to_vec(),
+    }
+}
+
+fn vec_to_signature(data: &[u8]) -> SessionIdSignature {
+    SessionIdSignature {
+        signature: data.to_vec(),
+    }
+}
+
+/// Decrypt a pair of AES-256 keys encrypted with the AuthGraph PBK.
+pub fn decipher_aes_keys(imp: &traits::TraitImpl, arc: &[Vec<u8>; 2]) -> [key::AesKey; 2] {
+    [
+        decipher_aes_key(imp, &arc[0]),
+        decipher_aes_key(imp, &arc[1]),
+    ]
+}
+
+/// Decrypt an AES-256 key encrypted with the AuthGraph PBK.
+pub fn decipher_aes_key(imp: &traits::TraitImpl, arc: &[u8]) -> key::AesKey {
+    let pbk = imp.device.get_per_boot_key().expect("no PBK available");
+    let arc::ArcContent {
+        payload,
+        protected_headers: _,
+        unprotected_headers: _,
+    } = arc::decipher_arc(&pbk, arc, &*imp.aes_gcm).expect("failed to decrypt arc");
+    assert_eq!(payload.0.len(), 32);
+    let mut key = key::AesKey([0; 32]);
+    key.0.copy_from_slice(&payload.0);
+    assert_ne!(key.0, [0; 32], "agreed AES-256 key should be non-zero");
+    key
+}
diff --git a/security/authgraph/aidl/vts/functional/role_test.rs b/security/authgraph/aidl/vts/functional/role_test.rs
new file mode 100644
index 0000000..e95361a
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/role_test.rs
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! Tests of individual AuthGraph role (source or sink) functionality.
+
+#![cfg(test)]
+
+use authgraph_vts_test as vts;
+use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+    IAuthGraphKeyExchange::IAuthGraphKeyExchange,
+};
+
+const AUTH_GRAPH_NONSECURE: &str =
+    "android.hardware.security.authgraph.IAuthGraphKeyExchange/nonsecure";
+
+/// Retrieve the /nonsecure instance of AuthGraph, which supports both sink and source roles.
+fn get_nonsecure() -> Option<binder::Strong<dyn IAuthGraphKeyExchange>> {
+    binder::get_interface(AUTH_GRAPH_NONSECURE).ok()
+}
+
+/// Macro to require availability of a /nonsecure instance of AuthGraph.
+///
+/// Note that this macro triggers `return` if not found.
+macro_rules! require_nonsecure {
+    {} => {
+        match get_nonsecure() {
+            Some(v) => v,
+            None => {
+                eprintln!("Skipping test as no /nonsecure impl found");
+                return;
+            }
+        }
+    }
+}
+
+#[test]
+fn test_nonsecure_source_mainline() {
+    let mut impls = vts::test_impls();
+    vts::source::test_mainline(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_source_corrupt_sig() {
+    let mut impls = vts::test_impls();
+    vts::source::test_corrupt_sig(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_source_corrupt_keys() {
+    let mut impls = vts::test_impls();
+    vts::source::test_corrupt_key(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_sink_mainline() {
+    let mut impls = vts::test_impls();
+    vts::sink::test_mainline(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_sink_corrupt_sig() {
+    let mut impls = vts::test_impls();
+    vts::sink::test_corrupt_sig(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_sink_corrupt_keys() {
+    let mut impls = vts::test_impls();
+    vts::sink::test_corrupt_keys(&mut impls, require_nonsecure!());
+}
diff --git a/security/authgraph/aidl/vts/functional/sink.rs b/security/authgraph/aidl/vts/functional/sink.rs
new file mode 100644
index 0000000..5c81593
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/sink.rs
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! VTS tests for sinks
+use super::*;
+use authgraph_core::traits;
+
+/// Run AuthGraph tests against the provided sink, using a local test source implementation.
+pub fn test(impls: &mut traits::TraitImpl, sink: binder::Strong<dyn IAuthGraphKeyExchange>) {
+    test_mainline(impls, sink.clone());
+    test_corrupt_sig(impls, sink.clone());
+    test_corrupt_keys(impls, sink);
+}
+
+/// Perform mainline AuthGraph key exchange with the provided sink and local implementation.
+/// Return the agreed AES keys in plaintext.
+pub fn test_mainline(
+    impls: &mut traits::TraitImpl,
+    sink: binder::Strong<dyn IAuthGraphKeyExchange>,
+) -> [key::AesKey; 2] {
+    // Step 1: create an ephemeral ECDH key at the (local) source.
+    let source_init_info = ke::create(impls).expect("failed to create() with local impl");
+
+    // Step 2: pass the source's ECDH public key and other session info to the (remote) sink.
+    let init_result = sink
+        .init(
+            &build_plain_pub_key(&source_init_info.ke_key.pub_key),
+            &vec_to_identity(&source_init_info.identity),
+            &source_init_info.nonce,
+            source_init_info.version,
+        )
+        .expect("failed to init() with remote impl");
+    let sink_init_info = init_result.sessionInitiationInfo;
+    let sink_pub_key = extract_plain_pub_key(&sink_init_info.key.pubKey);
+
+    let sink_info = init_result.sessionInfo;
+    assert!(!sink_info.sessionId.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let sink_verification_key =
+        verification_key_from_identity(&impls, &sink_init_info.identity.identity);
+    ke::verify_signature_on_session_id(
+        &sink_verification_key,
+        &sink_info.sessionId,
+        &sink_info.signature.signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (local) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = ke::finish(
+        impls,
+        &sink_pub_key.plainPubKey,
+        &sink_init_info.identity.identity,
+        &sink_info.signature.signature,
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        source_init_info.ke_key,
+    )
+    .expect("failed to finish() with local impl");
+    assert!(!source_info.session_id.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let source_verification_key =
+        verification_key_from_identity(&impls, &source_init_info.identity);
+    ke::verify_signature_on_session_id(
+        &source_verification_key,
+        &source_info.session_id,
+        &source_info.session_id_signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Both ends should agree on the session ID.
+    assert_eq!(source_info.session_id, sink_info.sessionId);
+
+    // Step 4: pass the (local) source's session ID signature back to the sink, so it can check it
+    // and update the symmetric keys so they're marked as authentication complete.
+    let _sink_arcs = sink
+        .authenticationComplete(
+            &vec_to_signature(&source_info.session_id_signature),
+            &sink_info.sharedKeys,
+        )
+        .expect("failed to authenticationComplete() with remote sink");
+
+    // Decrypt and return the session keys.
+    decipher_aes_keys(&impls, &source_info.shared_keys)
+}
+
+/// Perform mainline AuthGraph key exchange with the provided sink, but provide an invalid
+/// session ID signature.
+pub fn test_corrupt_sig(
+    impls: &mut traits::TraitImpl,
+    sink: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (local) source.
+    let source_init_info = ke::create(impls).expect("failed to create() with local impl");
+
+    // Step 2: pass the source's ECDH public key and other session info to the (remote) sink.
+    let init_result = sink
+        .init(
+            &build_plain_pub_key(&source_init_info.ke_key.pub_key),
+            &vec_to_identity(&source_init_info.identity),
+            &source_init_info.nonce,
+            source_init_info.version,
+        )
+        .expect("failed to init() with remote impl");
+    let sink_init_info = init_result.sessionInitiationInfo;
+    let sink_pub_key = extract_plain_pub_key(&sink_init_info.key.pubKey);
+
+    let sink_info = init_result.sessionInfo;
+    assert!(!sink_info.sessionId.is_empty());
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (local) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = ke::finish(
+        impls,
+        &sink_pub_key.plainPubKey,
+        &sink_init_info.identity.identity,
+        &sink_info.signature.signature,
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        source_init_info.ke_key,
+    )
+    .expect("failed to finish() with local impl");
+    assert!(!source_info.session_id.is_empty());
+
+    // Build a corrupted version of the (local) source's session ID signature.
+    let mut corrupt_signature = source_info.session_id_signature.clone();
+    let sig_len = corrupt_signature.len();
+    corrupt_signature[sig_len - 1] ^= 0x01;
+
+    // Step 4: pass the (local) source's **invalid** session ID signature back to the sink,
+    // which should reject it.
+    let result =
+        sink.authenticationComplete(&vec_to_signature(&corrupt_signature), &sink_info.sharedKeys);
+    let err = result.expect_err("expect failure with corrupt signature");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_SIGNATURE.0, None)
+    );
+}
+
+/// Perform mainline AuthGraph key exchange with the provided sink, but provide an invalid
+/// Arc for the sink's key.
+pub fn test_corrupt_keys(
+    impls: &mut traits::TraitImpl,
+    sink: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (local) source.
+    let source_init_info = ke::create(impls).expect("failed to create() with local impl");
+
+    // Step 2: pass the source's ECDH public key and other session info to the (remote) sink.
+    let init_result = sink
+        .init(
+            &build_plain_pub_key(&source_init_info.ke_key.pub_key),
+            &vec_to_identity(&source_init_info.identity),
+            &source_init_info.nonce,
+            source_init_info.version,
+        )
+        .expect("failed to init() with remote impl");
+    let sink_init_info = init_result.sessionInitiationInfo;
+    let sink_pub_key = extract_plain_pub_key(&sink_init_info.key.pubKey);
+
+    let sink_info = init_result.sessionInfo;
+    assert!(!sink_info.sessionId.is_empty());
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (local) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = ke::finish(
+        impls,
+        &sink_pub_key.plainPubKey,
+        &sink_init_info.identity.identity,
+        &sink_info.signature.signature,
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        source_init_info.ke_key,
+    )
+    .expect("failed to finish() with local impl");
+    assert!(!source_info.session_id.is_empty());
+
+    // Deliberately corrupt the sink's shared key Arcs before returning them
+    let mut corrupt_keys = sink_info.sharedKeys.clone();
+    let len0 = corrupt_keys[0].arc.len();
+    let len1 = corrupt_keys[1].arc.len();
+    corrupt_keys[0].arc[len0 - 1] ^= 0x01;
+    corrupt_keys[1].arc[len1 - 1] ^= 0x01;
+
+    // Step 4: pass the (local) source's session ID signature back to the sink, but with corrupted
+    // keys, which should be rejected.
+    let result = sink.authenticationComplete(
+        &vec_to_signature(&source_info.session_id_signature),
+        &corrupt_keys,
+    );
+    let err = result.expect_err("expect failure with corrupt keys");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_SHARED_KEY_ARCS.0, None)
+    );
+}
diff --git a/security/authgraph/aidl/vts/functional/source.rs b/security/authgraph/aidl/vts/functional/source.rs
new file mode 100644
index 0000000..9aaaaee
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/source.rs
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 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.
+ */
+
+//! VTS tests for sources
+use super::*;
+use authgraph_core::traits;
+
+/// Run AuthGraph tests against the provided source, using a local test sink implementation.
+pub fn test(impls: &mut traits::TraitImpl, source: binder::Strong<dyn IAuthGraphKeyExchange>) {
+    test_mainline(impls, source.clone());
+    test_corrupt_sig(impls, source.clone());
+    test_corrupt_key(impls, source);
+}
+
+/// Perform mainline AuthGraph key exchange with the provided source.
+/// Return the agreed AES keys in plaintext.
+pub fn test_mainline(
+    impls: &mut traits::TraitImpl,
+    source: binder::Strong<dyn IAuthGraphKeyExchange>,
+) -> [key::AesKey; 2] {
+    // Step 1: create an ephemeral ECDH key at the (remote) source.
+    let source_init_info = source
+        .create()
+        .expect("failed to create() with remote impl");
+    assert!(source_init_info.key.pubKey.is_some());
+    assert!(source_init_info.key.arcFromPBK.is_some());
+    let source_pub_key = extract_plain_pub_key(&source_init_info.key.pubKey);
+
+    // Step 2: pass the source's ECDH public key and other session info to the (local) sink.
+    let init_result = ke::init(
+        impls,
+        &source_pub_key.plainPubKey,
+        &source_init_info.identity.identity,
+        &source_init_info.nonce,
+        source_init_info.version,
+    )
+    .expect("failed to init() with local impl");
+    let sink_init_info = init_result.session_init_info;
+    let sink_pub_key = sink_init_info
+        .ke_key
+        .pub_key
+        .expect("expect pub_key to be populated");
+
+    let sink_info = init_result.session_info;
+    assert!(!sink_info.session_id.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let sink_verification_key = verification_key_from_identity(&impls, &sink_init_info.identity);
+    ke::verify_signature_on_session_id(
+        &sink_verification_key,
+        &sink_info.session_id,
+        &sink_info.session_id_signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (remote) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = source
+        .finish(
+            &PubKey::PlainKey(PlainPubKey {
+                plainPubKey: sink_pub_key,
+            }),
+            &Identity {
+                identity: sink_init_info.identity,
+            },
+            &vec_to_signature(&sink_info.session_id_signature),
+            &sink_init_info.nonce,
+            sink_init_info.version,
+            &source_init_info.key,
+        )
+        .expect("failed to finish() with remote impl");
+    assert!(!source_info.sessionId.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let source_verification_key =
+        verification_key_from_identity(&impls, &source_init_info.identity.identity);
+    ke::verify_signature_on_session_id(
+        &source_verification_key,
+        &source_info.sessionId,
+        &source_info.signature.signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Both ends should agree on the session ID.
+    assert_eq!(source_info.sessionId, sink_info.session_id);
+
+    // Step 4: pass the (remote) source's session ID signature back to the sink, so it can check it
+    // and update the symmetric keys so they're marked as authentication complete.
+    let sink_arcs = ke::authentication_complete(
+        impls,
+        &source_info.signature.signature,
+        sink_info.shared_keys,
+    )
+    .expect("failed to authenticationComplete() with local sink");
+
+    // Decrypt and return the session keys.
+    decipher_aes_keys(&impls, &sink_arcs)
+}
+
+/// Perform mainline AuthGraph key exchange with the provided source, but provide an invalid session
+/// ID signature.
+pub fn test_corrupt_sig(
+    impls: &mut traits::TraitImpl,
+    source: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (remote) source.
+    let source_init_info = source
+        .create()
+        .expect("failed to create() with remote impl");
+    assert!(source_init_info.key.pubKey.is_some());
+    assert!(source_init_info.key.arcFromPBK.is_some());
+    let source_pub_key = extract_plain_pub_key(&source_init_info.key.pubKey);
+
+    // Step 2: pass the source's ECDH public key and other session info to the (local) sink.
+    let init_result = ke::init(
+        impls,
+        &source_pub_key.plainPubKey,
+        &source_init_info.identity.identity,
+        &source_init_info.nonce,
+        source_init_info.version,
+    )
+    .expect("failed to init() with local impl");
+    let sink_init_info = init_result.session_init_info;
+    let sink_pub_key = sink_init_info
+        .ke_key
+        .pub_key
+        .expect("expect pub_key to be populated");
+    let sink_info = init_result.session_info;
+    assert!(!sink_info.session_id.is_empty());
+
+    // Deliberately corrupt the sink's session ID signature.
+    let mut corrupt_signature = sink_info.session_id_signature.clone();
+    let sig_len = corrupt_signature.len();
+    corrupt_signature[sig_len - 1] ^= 0x01;
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (remote) source, so it
+    // can calculate the same pair of symmetric keys.
+    let result = source.finish(
+        &PubKey::PlainKey(PlainPubKey {
+            plainPubKey: sink_pub_key,
+        }),
+        &Identity {
+            identity: sink_init_info.identity,
+        },
+        &vec_to_signature(&corrupt_signature),
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        &source_init_info.key,
+    );
+    let err = result.expect_err("expect failure with corrupt signature");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_SIGNATURE.0, None)
+    );
+}
+
+/// Perform mainline AuthGraph key exchange with the provided source, but give it back
+/// a corrupted key.
+pub fn test_corrupt_key(
+    impls: &mut traits::TraitImpl,
+    source: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (remote) source.
+    let source_init_info = source
+        .create()
+        .expect("failed to create() with remote impl");
+    assert!(source_init_info.key.pubKey.is_some());
+    assert!(source_init_info.key.arcFromPBK.is_some());
+    let source_pub_key = extract_plain_pub_key(&source_init_info.key.pubKey);
+
+    // Step 2: pass the source's ECDH public key and other session info to the (local) sink.
+    let init_result = ke::init(
+        impls,
+        &source_pub_key.plainPubKey,
+        &source_init_info.identity.identity,
+        &source_init_info.nonce,
+        source_init_info.version,
+    )
+    .expect("failed to init() with local impl");
+    let sink_init_info = init_result.session_init_info;
+    let sink_pub_key = sink_init_info
+        .ke_key
+        .pub_key
+        .expect("expect pub_key to be populated");
+
+    let sink_info = init_result.session_info;
+    assert!(!sink_info.session_id.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let sink_verification_key = verification_key_from_identity(&impls, &sink_init_info.identity);
+    ke::verify_signature_on_session_id(
+        &sink_verification_key,
+        &sink_info.session_id,
+        &sink_info.session_id_signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Deliberately corrupt the source's encrypted key.
+    let mut corrupt_key = source_init_info.key.clone();
+    match &mut corrupt_key.arcFromPBK {
+        Some(a) => {
+            let len = a.arc.len();
+            a.arc[len - 1] ^= 0x01;
+        }
+        None => panic!("no arc data"),
+    }
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (remote) source, but
+    // give it back a corrupted version of its own key.
+    let result = source.finish(
+        &PubKey::PlainKey(PlainPubKey {
+            plainPubKey: sink_pub_key,
+        }),
+        &Identity {
+            identity: sink_init_info.identity,
+        },
+        &vec_to_signature(&sink_info.session_id_signature),
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        &corrupt_key,
+    );
+
+    let err = result.expect_err("expect failure with corrupt signature");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_PRIV_KEY_ARC_IN_KEY.0, None)
+    );
+}