373 lines
13 KiB
C++
373 lines
13 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
#include "../src/private/crypto/inc/crypto.hpp"
|
|
#include "attestation_collateral.hpp"
|
|
#include "azure/attestation/attestation_client.hpp"
|
|
|
|
#include <azure/core/internal/json/json.hpp>
|
|
#include <azure/core/test/test_base.hpp>
|
|
#include <azure/identity/client_secret_credential.hpp>
|
|
|
|
#include <tuple>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
using namespace Azure::Security::Attestation;
|
|
using namespace Azure::Security::Attestation::Models;
|
|
using namespace Azure::Core;
|
|
|
|
namespace Azure { namespace Security { namespace Attestation { namespace Test {
|
|
|
|
enum class InstanceType
|
|
{
|
|
Shared,
|
|
AAD,
|
|
Isolated
|
|
};
|
|
|
|
class AttestationTests
|
|
: public Azure::Core::Test::TestBase,
|
|
public testing::WithParamInterface<std::tuple<InstanceType, AttestationType>> {
|
|
private:
|
|
protected:
|
|
std::shared_ptr<Azure::Core::Credentials::TokenCredential> m_credential;
|
|
std::string m_endpoint;
|
|
|
|
// Create
|
|
virtual void SetUp() override
|
|
{
|
|
Azure::Core::Test::TestBase::SetUpTestBase(AZURE_TEST_RECORDING_DIR);
|
|
|
|
InstanceType instanceType(std::get<0>(GetParam()));
|
|
if (instanceType == InstanceType::Shared)
|
|
{
|
|
std::string shortLocation(GetEnv("LOCATION_SHORT_NAME"));
|
|
m_endpoint = "https://shared" + shortLocation + "." + shortLocation + ".attest.azure.net";
|
|
}
|
|
else if (instanceType == InstanceType::AAD)
|
|
{
|
|
m_endpoint = GetEnv("ATTESTATION_AAD_URL");
|
|
}
|
|
else if (instanceType == InstanceType::Isolated)
|
|
{
|
|
m_endpoint = GetEnv("ATTESTATION_ISOLATED_URL");
|
|
}
|
|
}
|
|
|
|
AttestationTokenValidationOptions GetTokenValidationOptions()
|
|
{
|
|
AttestationTokenValidationOptions returnValue;
|
|
if (m_testContext.IsPlaybackMode())
|
|
{
|
|
// Skip validating time stamps if using recordings.
|
|
returnValue.ValidateNotBeforeTime = false;
|
|
returnValue.ValidateExpirationTime = false;
|
|
}
|
|
else
|
|
{
|
|
returnValue.TimeValidationSlack = 10s;
|
|
}
|
|
return returnValue;
|
|
}
|
|
|
|
AttestationClient CreateClient()
|
|
{
|
|
// `InitTestClient` takes care of setting up Record&Playback.
|
|
auto options = InitClientOptions<Azure::Security::Attestation::AttestationClientOptions>();
|
|
options.TokenValidationOptions = GetTokenValidationOptions();
|
|
return AttestationClient::Create(m_endpoint, options);
|
|
}
|
|
AttestationClient CreateAuthenticatedClient()
|
|
{
|
|
// `InitClientOptions` takes care of setting up Record&Playback.
|
|
AttestationClientOptions options = InitClientOptions<AttestationClientOptions>();
|
|
options.TokenValidationOptions = GetTokenValidationOptions();
|
|
std::shared_ptr<Azure::Core::Credentials::TokenCredential> credential
|
|
= CreateClientSecretCredential(
|
|
GetEnv("AZURE_TENANT_ID"), GetEnv("AZURE_CLIENT_ID"), GetEnv("AZURE_CLIENT_SECRET"));
|
|
|
|
return AttestationClient::Create(m_endpoint, credential, options);
|
|
}
|
|
|
|
void ValidateAttestResponse(
|
|
Azure::Response<AttestationToken<AttestationResult>> const& response,
|
|
Azure::Nullable<AttestationData> data = {},
|
|
std::string draftPolicy = {})
|
|
{
|
|
EXPECT_TRUE(response.Value.Issuer);
|
|
if (!m_testContext.IsPlaybackMode())
|
|
{
|
|
EXPECT_EQ(m_endpoint, *response.Value.Issuer);
|
|
}
|
|
|
|
EXPECT_TRUE(response.Value.Body.SgxMrEnclave);
|
|
EXPECT_TRUE(response.Value.Body.SgxMrSigner);
|
|
EXPECT_TRUE(response.Value.Body.SgxSvn);
|
|
EXPECT_TRUE(response.Value.Body.SgxProductId);
|
|
if (data)
|
|
{
|
|
if (data->DataType == AttestationDataType::Json)
|
|
{
|
|
EXPECT_TRUE(response.Value.Body.RunTimeClaims);
|
|
EXPECT_FALSE(response.Value.Body.EnclaveHeldData);
|
|
// canonicalize the JSON sent to the service before checking with the service output.
|
|
auto sentJson(Azure::Core::Json::_internal::json::parse(data->Data));
|
|
EXPECT_EQ(sentJson.dump(), *response.Value.Body.RunTimeClaims);
|
|
}
|
|
else
|
|
{
|
|
EXPECT_FALSE(response.Value.Body.RunTimeClaims);
|
|
EXPECT_TRUE(response.Value.Body.EnclaveHeldData);
|
|
// If we expected binary, the EnclaveHeldData in the response should be the value sent.
|
|
EXPECT_EQ(data->Data, *response.Value.Body.EnclaveHeldData);
|
|
}
|
|
}
|
|
if (!draftPolicy.empty())
|
|
{
|
|
EXPECT_TRUE(response.Value.Body.PolicyClaims);
|
|
auto policyClaims(
|
|
Azure::Core::Json::_internal::json::parse(*response.Value.Body.PolicyClaims));
|
|
EXPECT_TRUE(policyClaims.contains("custom-name"));
|
|
}
|
|
}
|
|
};
|
|
|
|
TEST_P(AttestationTests, SimpleAttest)
|
|
{
|
|
auto client(CreateClient());
|
|
|
|
AttestationType type = std::get<1>(GetParam());
|
|
if (type == AttestationType::OpenEnclave)
|
|
{
|
|
auto report = AttestationCollateral::OpenEnclaveReport();
|
|
auto attestResponse = client.AttestOpenEnclave(report);
|
|
ValidateAttestResponse(attestResponse);
|
|
}
|
|
else if (type == AttestationType::SgxEnclave)
|
|
{
|
|
auto quote = AttestationCollateral::SgxQuote();
|
|
auto attestResponse = client.AttestSgxEnclave(quote);
|
|
ValidateAttestResponse(attestResponse);
|
|
}
|
|
}
|
|
|
|
TEST_P(AttestationTests, AttestWithRuntimeData)
|
|
{
|
|
// Attestation clients don't need to be authenticated, but they can be.
|
|
auto client(CreateAuthenticatedClient());
|
|
auto runtimeData = AttestationCollateral::RunTimeData();
|
|
|
|
AttestationType type = std::get<1>(GetParam());
|
|
AttestationData data{runtimeData, AttestationDataType::Binary};
|
|
if (type == AttestationType::OpenEnclave)
|
|
{
|
|
AttestOpenEnclaveOptions options;
|
|
options.RunTimeData = data;
|
|
auto report = AttestationCollateral::OpenEnclaveReport();
|
|
auto attestResponse = client.AttestOpenEnclave(report, options);
|
|
ValidateAttestResponse(attestResponse, data);
|
|
}
|
|
else if (type == AttestationType::SgxEnclave)
|
|
{
|
|
AttestSgxEnclaveOptions options;
|
|
options.RunTimeData = data;
|
|
auto quote = AttestationCollateral::SgxQuote();
|
|
auto attestResponse = client.AttestSgxEnclave(quote, options);
|
|
ValidateAttestResponse(attestResponse, data);
|
|
}
|
|
}
|
|
|
|
TEST_P(AttestationTests, AttestWithDraftPolicy)
|
|
{
|
|
// Attestation clients don't need to be authenticated, but they can be.
|
|
auto client(CreateAuthenticatedClient());
|
|
auto runtimeData = AttestationCollateral::RunTimeData();
|
|
|
|
AttestationType type = std::get<1>(GetParam());
|
|
|
|
if (type == AttestationType::OpenEnclave)
|
|
{
|
|
AttestOpenEnclaveOptions options;
|
|
options.DraftPolicyForAttestation = R"(version= 1.0;
|
|
authorizationrules
|
|
{
|
|
[ type=="x-ms-sgx-is-debuggable", value==true] &&
|
|
[ type=="x-ms-sgx-product-id", value!=0 ] &&
|
|
[ type=="x-ms-sgx-svn", value>= 0 ] &&
|
|
[ type=="x-ms-sgx-mrsigner", value == "4aea5f9a0ed04b11f889aadfe6a1d376213a29a95a85ce7337ae6f7fece6610c"]
|
|
=> permit();
|
|
};
|
|
issuancerules {
|
|
c:[type=="x-ms-sgx-mrsigner"] => issue(type="custom-name", value=c.value);
|
|
};)";
|
|
auto report = AttestationCollateral::OpenEnclaveReport();
|
|
|
|
auto attestResponse = client.AttestOpenEnclave(report, options);
|
|
// Because a draft policy was set, the resulting token is unsigned.
|
|
ValidateAttestResponse(
|
|
attestResponse, Azure::Nullable<AttestationData>(), *options.DraftPolicyForAttestation);
|
|
|
|
options.DraftPolicyForAttestation = R"(version= 1.0;
|
|
authorizationrules
|
|
{
|
|
[ type=="x-ms-sgx-is-debuggable", value==false ] &&
|
|
[ type=="x-ms-sgx-product-id", value==0 ] &&
|
|
[ type=="x-ms-sgx-svn", value>= 0 ]
|
|
=> permit();
|
|
};
|
|
issuancerules {
|
|
c:[type=="x-ms-sgx-mrsigner"] => issue(type="custom-name", value=c.value);
|
|
};)";
|
|
EXPECT_THROW(client.AttestOpenEnclave(report, options), Azure::Core::RequestFailedException);
|
|
}
|
|
else if (type == AttestationType::SgxEnclave)
|
|
{
|
|
AttestSgxEnclaveOptions options;
|
|
options.DraftPolicyForAttestation = R"(version= 1.0;
|
|
authorizationrules
|
|
{
|
|
[ type=="x-ms-sgx-is-debuggable", value==true] &&
|
|
[ type=="x-ms-sgx-product-id", value!=0 ] &&
|
|
[ type=="x-ms-sgx-svn", value>= 0 ] &&
|
|
[ type=="x-ms-sgx-mrsigner", value == "4aea5f9a0ed04b11f889aadfe6a1d376213a29a95a85ce7337ae6f7fece6610c"]
|
|
=> permit();
|
|
};
|
|
issuancerules {
|
|
c:[type=="x-ms-sgx-mrsigner"] => issue(type="custom-name", value=c.value);
|
|
};)";
|
|
auto quote = AttestationCollateral::SgxQuote();
|
|
auto attestResponse = client.AttestSgxEnclave(quote, options);
|
|
ValidateAttestResponse(
|
|
attestResponse, Azure::Nullable<AttestationData>(), *options.DraftPolicyForAttestation);
|
|
|
|
options.DraftPolicyForAttestation = R"(version= 1.0;
|
|
authorizationrules
|
|
{
|
|
[ type=="x-ms-sgx-is-debuggable", value==false ] &&
|
|
[ type=="x-ms-sgx-product-id", value==0 ] &&
|
|
[ type=="x-ms-sgx-svn", value>= 0 ]
|
|
=> permit();
|
|
};
|
|
issuancerules {
|
|
c:[type=="x-ms-sgx-mrsigner"] => issue(type="custom-name", value=c.value);
|
|
};)";
|
|
EXPECT_THROW(client.AttestSgxEnclave(quote, options), Azure::Core::RequestFailedException);
|
|
}
|
|
}
|
|
|
|
TEST_P(AttestationTests, AttestWithRuntimeDataJson)
|
|
{
|
|
auto client(CreateClient());
|
|
auto runtimeData = AttestationCollateral::RunTimeData();
|
|
|
|
AttestationType type = std::get<1>(GetParam());
|
|
AttestationData data{runtimeData, AttestationDataType::Json};
|
|
if (type == AttestationType::OpenEnclave)
|
|
{
|
|
auto report = AttestationCollateral::OpenEnclaveReport();
|
|
AttestOpenEnclaveOptions options;
|
|
options.RunTimeData = data;
|
|
options.TokenValidationOptionsOverride = GetTokenValidationOptions();
|
|
(*options.TokenValidationOptionsOverride).ValidationCallback
|
|
= [&](AttestationToken<void> const& token, AttestationSigner const& signer) {
|
|
EXPECT_TRUE(token.Issuer);
|
|
// When running against a live server, the m_endpoint value is mocked, so we cannot
|
|
// compare against it.
|
|
// The signers KeyId MUST match the key ID in the token.
|
|
EXPECT_TRUE(signer.KeyId);
|
|
EXPECT_TRUE(token.Header.KeyId);
|
|
EXPECT_EQ(*signer.KeyId, *token.Header.KeyId);
|
|
EXPECT_TRUE(signer.CertificateChain);
|
|
auto cert(Azure::Security::Attestation::_detail::Cryptography::ImportX509Certificate(
|
|
(*signer.CertificateChain)[0]));
|
|
if (!m_testContext.IsPlaybackMode())
|
|
{
|
|
// The issuer of an attestation token MUST be the endpoint which we're talking to.
|
|
EXPECT_EQ(*token.Issuer, m_endpoint);
|
|
// And the endpoint should be in the subject of hte issuer.
|
|
EXPECT_NE(cert->GetSubjectName().find(m_endpoint), std::string::npos);
|
|
}
|
|
};
|
|
auto attestResponse = client.AttestOpenEnclave(report, options);
|
|
ValidateAttestResponse(attestResponse, data);
|
|
}
|
|
else if (type == AttestationType::SgxEnclave)
|
|
{
|
|
auto quote = AttestationCollateral::SgxQuote();
|
|
auto attestResponse = client.AttestSgxEnclave(quote, {data});
|
|
ValidateAttestResponse(attestResponse, data);
|
|
}
|
|
}
|
|
|
|
TEST_P(AttestationTests, CreateAttestationClients)
|
|
{
|
|
// `InitTestClient` takes care of setting up Record&Playback.
|
|
auto options = InitClientOptions<Azure::Security::Attestation::AttestationClientOptions>();
|
|
|
|
{
|
|
AttestationClient client = AttestationClient::Create(this->m_endpoint, options);
|
|
EXPECT_EQ(m_endpoint, client.Endpoint());
|
|
}
|
|
{
|
|
AttestationClient const client = AttestationClient::Create(this->m_endpoint, options);
|
|
EXPECT_EQ(m_endpoint, client.Endpoint());
|
|
}
|
|
{
|
|
AttestationClient client = AttestationClient::Create(this->m_endpoint, options);
|
|
EXPECT_EQ(m_endpoint, client.Endpoint());
|
|
}
|
|
{
|
|
auto client = AttestationClient::Create(this->m_endpoint, options);
|
|
EXPECT_EQ(m_endpoint, client.Endpoint());
|
|
}
|
|
{
|
|
auto const client = AttestationClient::Create(this->m_endpoint, options);
|
|
EXPECT_EQ(m_endpoint, client.Endpoint());
|
|
}
|
|
|
|
{
|
|
std::unique_ptr<AttestationClient> client = std::make_unique<AttestationClient>(
|
|
AttestationClient::Create(this->m_endpoint, options));
|
|
EXPECT_EQ(m_endpoint, client->Endpoint());
|
|
}
|
|
{
|
|
std::unique_ptr<AttestationClient const> client = std::make_unique<AttestationClient>(
|
|
AttestationClient::Create(this->m_endpoint, options));
|
|
EXPECT_EQ(m_endpoint, client->Endpoint());
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
static std::string GetSuffix(const testing::TestParamInfo<AttestationTests::ParamType>& info)
|
|
{
|
|
std::string rv = std::get<1>(info.param).ToString();
|
|
rv += "_";
|
|
switch (std::get<0>(info.param))
|
|
{
|
|
case InstanceType::Shared:
|
|
rv += "Shared";
|
|
break;
|
|
case InstanceType::AAD:
|
|
rv += "Aad";
|
|
break;
|
|
case InstanceType::Isolated:
|
|
rv += "Isolated";
|
|
break;
|
|
default:
|
|
throw std::runtime_error("Unknown instance type");
|
|
}
|
|
return rv;
|
|
}
|
|
} // namespace
|
|
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
Attestation,
|
|
AttestationTests,
|
|
::testing::Combine(
|
|
::testing::Values(InstanceType::Shared, InstanceType::AAD, InstanceType::Isolated),
|
|
::testing::Values(AttestationType::OpenEnclave, AttestationType::SgxEnclave)),
|
|
GetSuffix);
|
|
|
|
}}}} // namespace Azure::Security::Attestation::Test
|