blob: d9dea7735af2d934f7bc2174005cf214cbf425e5 [file] [log] [blame]
/*
* 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.
*/
#define LOG_TAG "authgraph_session_test"
#include <android-base/logging.h>
#include <aidl/Gtest.h>
#include <aidl/Vintf.h>
#include <aidl/android/hardware/security/authgraph/Error.h>
#include <aidl/android/hardware/security/authgraph/IAuthGraphKeyExchange.h>
#include <android/binder_manager.h>
#include <binder/ProcessState.h>
#include <gtest/gtest.h>
#include <vector>
namespace aidl::android::hardware::security::authgraph::test {
using ::aidl::android::hardware::security::authgraph::Error;
namespace {
// Check that the signature in the encoded COSE_Sign1 data is correct, and that the payload matches.
// TODO: maybe drop separate payload, and extract it from cose_sign1.payload (and return it).
void CheckSignature(std::vector<uint8_t>& /*pub_cose_key*/, std::vector<uint8_t>& /*payload*/,
std::vector<uint8_t>& /*cose_sign1*/) {
// TODO: implement me
}
void CheckSignature(std::vector<uint8_t>& pub_cose_key, std::vector<uint8_t>& payload,
SessionIdSignature& signature) {
return CheckSignature(pub_cose_key, payload, signature.signature);
}
std::vector<uint8_t> SigningKeyFromIdentity(const Identity& identity) {
// TODO: This is a CBOR-encoded `Identity` which currently happens to be a COSE_Key with the
// pubkey This will change in future.
return identity.identity;
}
} // namespace
class AuthGraphSessionTest : public ::testing::TestWithParam<std::string> {
public:
enum ErrorType { AIDL_ERROR, BINDER_ERROR };
union ErrorValue {
Error aidl_error;
int32_t binder_error;
};
struct ReturnedError {
ErrorType err_type;
ErrorValue err_val;
friend bool operator==(const ReturnedError& lhs, const ReturnedError& rhs) {
return lhs.err_type == rhs.err_type;
switch (lhs.err_type) {
case ErrorType::AIDL_ERROR:
return lhs.err_val.aidl_error == rhs.err_val.aidl_error;
case ErrorType::BINDER_ERROR:
return lhs.err_val.binder_error == rhs.err_val.binder_error;
}
}
};
const ReturnedError OK = {.err_type = ErrorType::AIDL_ERROR, .err_val.aidl_error = Error::OK};
ReturnedError GetReturnError(const ::ndk::ScopedAStatus& result) {
if (result.isOk()) {
return OK;
}
int32_t exception_code = result.getExceptionCode();
int32_t error_code = result.getServiceSpecificError();
if (exception_code == EX_SERVICE_SPECIFIC && error_code != 0) {
ReturnedError re = {.err_type = ErrorType::AIDL_ERROR,
.err_val.aidl_error = static_cast<Error>(error_code)};
return re;
}
ReturnedError re = {.err_type = ErrorType::BINDER_ERROR,
.err_val.binder_error = exception_code};
return re;
}
// Build the parameters for the VTS test by enumerating the available HAL instances
static std::vector<std::string> build_params() {
auto params = ::android::getAidlHalInstanceNames(IAuthGraphKeyExchange::descriptor);
return params;
}
void SetUp() override {
ASSERT_TRUE(AServiceManager_isDeclared(GetParam().c_str()))
<< "No instance declared for " << GetParam();
::ndk::SpAIBinder binder(AServiceManager_waitForService(GetParam().c_str()));
authNode_ = IAuthGraphKeyExchange::fromBinder(binder);
ASSERT_NE(authNode_, nullptr) << "Failed to get Binder reference for " << GetParam();
}
void TearDown() override {}
protected:
std::shared_ptr<IAuthGraphKeyExchange> authNode_;
};
TEST_P(AuthGraphSessionTest, Mainline) {
std::shared_ptr<IAuthGraphKeyExchange> source = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> sink = authNode_;
// Step 1: create an ephemeral ECDH key at the source.
SessionInitiationInfo source_init_info;
ASSERT_EQ(OK, GetReturnError(source->create(&source_init_info)));
ASSERT_TRUE(source_init_info.key.pubKey.has_value());
ASSERT_TRUE(source_init_info.key.arcFromPBK.has_value());
// Step 2: pass the source's ECDH public key and other session info to the sink.
KeInitResult init_result;
ASSERT_EQ(OK, GetReturnError(sink->init(source_init_info.key.pubKey.value(),
source_init_info.identity, source_init_info.nonce,
source_init_info.version, &init_result)));
SessionInitiationInfo sink_init_info = init_result.sessionInitiationInfo;
ASSERT_TRUE(sink_init_info.key.pubKey.has_value());
// The sink_init_info.arcFromPBK need not be populated, as the ephemeral key agreement
// key is no longer needed.
SessionInfo sink_info = init_result.sessionInfo;
ASSERT_EQ((int)sink_info.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
ASSERT_GT((int)sink_info.sessionId.size(), 0) << "Expect non-empty session ID from sink";
std::vector<uint8_t> sink_signing_key = SigningKeyFromIdentity(sink_init_info.identity);
CheckSignature(sink_signing_key, sink_info.sessionId, sink_info.signature);
// Step 3: pass the sink's ECDH public key and other session info to the source, so it can
// calculate the same pair of symmetric keys.
SessionInfo source_info;
ASSERT_EQ(OK, GetReturnError(source->finish(sink_init_info.key.pubKey.value(),
sink_init_info.identity, sink_info.signature,
sink_init_info.nonce, sink_init_info.version,
source_init_info.key, &source_info)));
ASSERT_EQ((int)source_info.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
ASSERT_GT((int)source_info.sessionId.size(), 0) << "Expect non-empty session ID from source";
std::vector<uint8_t> source_signing_key = SigningKeyFromIdentity(source_init_info.identity);
CheckSignature(source_signing_key, source_info.sessionId, source_info.signature);
// Both ends should agree on the session ID.
ASSERT_EQ(source_info.sessionId, sink_info.sessionId);
// Step 4: pass the source's session ID info back to the sink, so it can check it and
// update the symmetric keys so they're marked as authentication complete.
std::array<Arc, 2> auth_complete_result;
ASSERT_EQ(OK, GetReturnError(sink->authenticationComplete(
source_info.signature, sink_info.sharedKeys, &auth_complete_result)));
ASSERT_EQ((int)auth_complete_result.size(), 2)
<< "Expect two symmetric keys from authComplete()";
sink_info.sharedKeys = auth_complete_result;
// At this point the sink and source have agreed on the same pair of symmetric keys,
// encoded as `sink_info.sharedKeys` and `source_info.sharedKeys`.
}
TEST_P(AuthGraphSessionTest, ParallelSink) {
std::shared_ptr<IAuthGraphKeyExchange> source = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> sink1 = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> sink2 = authNode_;
// Step 1: create ephemeral ECDH keys at the source.
SessionInitiationInfo source_init1_info;
ASSERT_EQ(OK, GetReturnError(source->create(&source_init1_info)));
ASSERT_TRUE(source_init1_info.key.pubKey.has_value());
ASSERT_TRUE(source_init1_info.key.arcFromPBK.has_value());
SessionInitiationInfo source_init2_info;
ASSERT_EQ(OK, GetReturnError(source->create(&source_init2_info)));
ASSERT_TRUE(source_init2_info.key.pubKey.has_value());
ASSERT_TRUE(source_init2_info.key.arcFromPBK.has_value());
// Step 2: pass the source's ECDH public keys and other session info to the sinks.
KeInitResult init1_result;
ASSERT_EQ(OK, GetReturnError(sink1->init(source_init1_info.key.pubKey.value(),
source_init1_info.identity, source_init1_info.nonce,
source_init1_info.version, &init1_result)));
SessionInitiationInfo sink1_init_info = init1_result.sessionInitiationInfo;
ASSERT_TRUE(sink1_init_info.key.pubKey.has_value());
SessionInfo sink1_info = init1_result.sessionInfo;
ASSERT_EQ((int)sink1_info.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
ASSERT_GT((int)sink1_info.sessionId.size(), 0) << "Expect non-empty session ID from sink";
std::vector<uint8_t> sink1_signing_key = SigningKeyFromIdentity(sink1_init_info.identity);
CheckSignature(sink1_signing_key, sink1_info.sessionId, sink1_info.signature);
KeInitResult init2_result;
ASSERT_EQ(OK, GetReturnError(sink2->init(source_init2_info.key.pubKey.value(),
source_init2_info.identity, source_init2_info.nonce,
source_init2_info.version, &init2_result)));
SessionInitiationInfo sink2_init_info = init2_result.sessionInitiationInfo;
ASSERT_TRUE(sink2_init_info.key.pubKey.has_value());
SessionInfo sink2_info = init2_result.sessionInfo;
ASSERT_EQ((int)sink2_info.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
ASSERT_GT((int)sink2_info.sessionId.size(), 0) << "Expect non-empty session ID from sink";
std::vector<uint8_t> sink2_signing_key = SigningKeyFromIdentity(sink2_init_info.identity);
CheckSignature(sink2_signing_key, sink2_info.sessionId, sink2_info.signature);
// Step 3: pass each sink's ECDH public key and other session info to the source, so it can
// calculate the same pair of symmetric keys.
SessionInfo source_info1;
ASSERT_EQ(OK, GetReturnError(source->finish(sink1_init_info.key.pubKey.value(),
sink1_init_info.identity, sink1_info.signature,
sink1_init_info.nonce, sink1_init_info.version,
source_init1_info.key, &source_info1)));
ASSERT_EQ((int)source_info1.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
ASSERT_GT((int)source_info1.sessionId.size(), 0) << "Expect non-empty session ID from source";
std::vector<uint8_t> source_signing_key1 = SigningKeyFromIdentity(source_init1_info.identity);
CheckSignature(source_signing_key1, source_info1.sessionId, source_info1.signature);
SessionInfo source_info2;
ASSERT_EQ(OK, GetReturnError(source->finish(sink2_init_info.key.pubKey.value(),
sink2_init_info.identity, sink2_info.signature,
sink2_init_info.nonce, sink2_init_info.version,
source_init2_info.key, &source_info2)));
ASSERT_EQ((int)source_info2.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
ASSERT_GT((int)source_info2.sessionId.size(), 0) << "Expect non-empty session ID from source";
std::vector<uint8_t> source_signing_key2 = SigningKeyFromIdentity(source_init2_info.identity);
CheckSignature(source_signing_key2, source_info2.sessionId, source_info2.signature);
// Both ends should agree on the session ID.
ASSERT_EQ(source_info1.sessionId, sink1_info.sessionId);
ASSERT_EQ(source_info2.sessionId, sink2_info.sessionId);
// Step 4: pass the source's session ID info back to the sink, so it can check it and
// update the symmetric keys so they're marked as authentication complete.
std::array<Arc, 2> auth_complete_result1;
ASSERT_EQ(OK, GetReturnError(sink1->authenticationComplete(
source_info1.signature, sink1_info.sharedKeys, &auth_complete_result1)));
ASSERT_EQ((int)auth_complete_result1.size(), 2)
<< "Expect two symmetric keys from authComplete()";
sink1_info.sharedKeys = auth_complete_result1;
std::array<Arc, 2> auth_complete_result2;
ASSERT_EQ(OK, GetReturnError(sink2->authenticationComplete(
source_info2.signature, sink2_info.sharedKeys, &auth_complete_result2)));
ASSERT_EQ((int)auth_complete_result2.size(), 2)
<< "Expect two symmetric keys from authComplete()";
sink2_info.sharedKeys = auth_complete_result2;
}
TEST_P(AuthGraphSessionTest, ParallelSource) {
std::shared_ptr<IAuthGraphKeyExchange> source1 = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> source2 = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> sink = authNode_;
// Step 1: create an ephemeral ECDH key at each of the sources.
SessionInitiationInfo source1_init_info;
ASSERT_EQ(OK, GetReturnError(source1->create(&source1_init_info)));
ASSERT_TRUE(source1_init_info.key.pubKey.has_value());
ASSERT_TRUE(source1_init_info.key.arcFromPBK.has_value());
SessionInitiationInfo source2_init_info;
ASSERT_EQ(OK, GetReturnError(source1->create(&source2_init_info)));
ASSERT_TRUE(source2_init_info.key.pubKey.has_value());
ASSERT_TRUE(source2_init_info.key.arcFromPBK.has_value());
// Step 2: pass each source's ECDH public key and other session info to the sink.
KeInitResult init1_result;
ASSERT_EQ(OK, GetReturnError(sink->init(source1_init_info.key.pubKey.value(),
source1_init_info.identity, source1_init_info.nonce,
source1_init_info.version, &init1_result)));
SessionInitiationInfo sink_init1_info = init1_result.sessionInitiationInfo;
ASSERT_TRUE(sink_init1_info.key.pubKey.has_value());
SessionInfo sink_info1 = init1_result.sessionInfo;
ASSERT_EQ((int)sink_info1.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
ASSERT_GT((int)sink_info1.sessionId.size(), 0) << "Expect non-empty session ID from sink";
std::vector<uint8_t> sink_signing_key1 = SigningKeyFromIdentity(sink_init1_info.identity);
CheckSignature(sink_signing_key1, sink_info1.sessionId, sink_info1.signature);
KeInitResult init2_result;
ASSERT_EQ(OK, GetReturnError(sink->init(source2_init_info.key.pubKey.value(),
source2_init_info.identity, source2_init_info.nonce,
source2_init_info.version, &init2_result)));
SessionInitiationInfo sink_init2_info = init2_result.sessionInitiationInfo;
ASSERT_TRUE(sink_init2_info.key.pubKey.has_value());
SessionInfo sink_info2 = init2_result.sessionInfo;
ASSERT_EQ((int)sink_info2.sharedKeys.size(), 2) << "Expect two symmetric keys from init()";
ASSERT_GT((int)sink_info2.sessionId.size(), 0) << "Expect non-empty session ID from sink";
std::vector<uint8_t> sink_signing_key2 = SigningKeyFromIdentity(sink_init2_info.identity);
CheckSignature(sink_signing_key2, sink_info2.sessionId, sink_info2.signature);
// Step 3: pass the sink's ECDH public keys and other session info to the each of the sources.
SessionInfo source1_info;
ASSERT_EQ(OK, GetReturnError(source1->finish(sink_init1_info.key.pubKey.value(),
sink_init1_info.identity, sink_info1.signature,
sink_init1_info.nonce, sink_init1_info.version,
source1_init_info.key, &source1_info)));
ASSERT_EQ((int)source1_info.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
ASSERT_GT((int)source1_info.sessionId.size(), 0) << "Expect non-empty session ID from source";
std::vector<uint8_t> source1_signing_key = SigningKeyFromIdentity(source1_init_info.identity);
CheckSignature(source1_signing_key, source1_info.sessionId, source1_info.signature);
SessionInfo source2_info;
ASSERT_EQ(OK, GetReturnError(source2->finish(sink_init2_info.key.pubKey.value(),
sink_init2_info.identity, sink_info2.signature,
sink_init2_info.nonce, sink_init2_info.version,
source2_init_info.key, &source2_info)));
ASSERT_EQ((int)source2_info.sharedKeys.size(), 2) << "Expect two symmetric keys from finsh()";
ASSERT_GT((int)source2_info.sessionId.size(), 0) << "Expect non-empty session ID from source";
std::vector<uint8_t> source2_signing_key = SigningKeyFromIdentity(source2_init_info.identity);
CheckSignature(source2_signing_key, source2_info.sessionId, source2_info.signature);
// Both ends should agree on the session ID.
ASSERT_EQ(source1_info.sessionId, sink_info1.sessionId);
ASSERT_EQ(source2_info.sessionId, sink_info2.sessionId);
// Step 4: pass the each source's session ID info back to the sink, so it can check it and
// update the symmetric keys so they're marked as authentication complete.
std::array<Arc, 2> auth_complete_result1;
ASSERT_EQ(OK, GetReturnError(sink->authenticationComplete(
source1_info.signature, sink_info1.sharedKeys, &auth_complete_result1)));
ASSERT_EQ((int)auth_complete_result1.size(), 2)
<< "Expect two symmetric keys from authComplete()";
sink_info1.sharedKeys = auth_complete_result1;
std::array<Arc, 2> auth_complete_result2;
ASSERT_EQ(OK, GetReturnError(sink->authenticationComplete(
source2_info.signature, sink_info2.sharedKeys, &auth_complete_result2)));
ASSERT_EQ((int)auth_complete_result2.size(), 2)
<< "Expect two symmetric keys from authComplete()";
sink_info2.sharedKeys = auth_complete_result2;
}
TEST_P(AuthGraphSessionTest, FreshNonces) {
std::shared_ptr<IAuthGraphKeyExchange> source = authNode_;
std::shared_ptr<IAuthGraphKeyExchange> sink = authNode_;
SessionInitiationInfo source_init_info1;
ASSERT_EQ(OK, GetReturnError(source->create(&source_init_info1)));
SessionInitiationInfo source_init_info2;
ASSERT_EQ(OK, GetReturnError(source->create(&source_init_info2)));
// Two calls to create() should result in the same identity but different nonce values.
ASSERT_EQ(source_init_info1.identity, source_init_info2.identity);
ASSERT_NE(source_init_info1.nonce, source_init_info2.nonce);
ASSERT_NE(source_init_info1.key.pubKey, source_init_info2.key.pubKey);
ASSERT_NE(source_init_info1.key.arcFromPBK, source_init_info2.key.arcFromPBK);
KeInitResult init_result1;
ASSERT_EQ(OK, GetReturnError(sink->init(source_init_info1.key.pubKey.value(),
source_init_info1.identity, source_init_info1.nonce,
source_init_info1.version, &init_result1)));
KeInitResult init_result2;
ASSERT_EQ(OK, GetReturnError(sink->init(source_init_info2.key.pubKey.value(),
source_init_info2.identity, source_init_info2.nonce,
source_init_info2.version, &init_result2)));
// Two calls to init() should result in the same identity buf different nonces and session IDs.
ASSERT_EQ(init_result1.sessionInitiationInfo.identity,
init_result2.sessionInitiationInfo.identity);
ASSERT_NE(init_result1.sessionInitiationInfo.nonce, init_result2.sessionInitiationInfo.nonce);
ASSERT_NE(init_result1.sessionInfo.sessionId, init_result2.sessionInfo.sessionId);
}
INSTANTIATE_TEST_SUITE_P(PerInstance, AuthGraphSessionTest,
testing::ValuesIn(AuthGraphSessionTest::build_params()),
::android::PrintInstanceNameToString);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AuthGraphSessionTest);
} // namespace aidl::android::hardware::security::authgraph::test
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}