AuthGraph: add per-role VTS tests

Add VTS tests that just exercise a single role, sink or source.

Use the AuthGraph core library to provide the implementation of the
other role.  This means that the tests are best written in Rust.

Put the test code into a library, so that other HALs which include
AuthGraph as a component can exercise the AuthGraph parts in their own
VTS tests.

Test: VtsAidlAuthGraphRoleTest
Bug: 284470121
Change-Id: I73f6aa277b41cc728587d707d7a6f82f0d18e08f
This commit is contained in:
David Drysdale 2023-11-07 09:27:56 +00:00
parent 6fb22dc9ef
commit fe41825f1c
5 changed files with 669 additions and 0 deletions

View file

@ -46,3 +46,36 @@ cc_test {
"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",
],
}

View file

@ -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
}

View file

@ -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!());
}

View file

@ -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)
);
}

View file

@ -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)
);
}