diff --git a/sdk/attestation/assets.json b/sdk/attestation/assets.json index 79c2ec50a2..864ffbff7a 100644 --- a/sdk/attestation/assets.json +++ b/sdk/attestation/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "cpp", "TagPrefix": "cpp/attestation", - "Tag": "cpp/attestation_f34b6b4684" + "Tag": "cpp/attestation_59bd1251d4" } diff --git a/sdk/attestation/azure-security-attestation/CHANGELOG.md b/sdk/attestation/azure-security-attestation/CHANGELOG.md index cf63e5680f..ec7a0f3647 100644 --- a/sdk/attestation/azure-security-attestation/CHANGELOG.md +++ b/sdk/attestation/azure-security-attestation/CHANGELOG.md @@ -4,6 +4,11 @@ ### Features Added +- Added `AttestationClient::AttestPluton` method for Pluton attestation support. + Pluton attestation is not currently supported for the default API version and can only be accessed via 2026-03-11-preview. +- Added `PlutonAttestationResult` model type containing the attestation response data. +- Added `AttestPlutonOptions` options type for configuring Pluton attestation requests. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client.hpp b/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client.hpp index 11a54c480c..930548b7fa 100644 --- a/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client.hpp +++ b/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client.hpp @@ -247,7 +247,30 @@ namespace Azure { namespace Security { namespace Attestation { AttestTpmOptions const& options = AttestTpmOptions{}, Azure::Core::Context const& context = Azure::Core::Context{}) const; + /** + * @brief Sends Pluton-based attestation data to the service. + * Pluton attestation is not currently supported for the default API version and can only be + * accessed via 2026-03-11-preview. + * + * @param dataToAttest - Attestation request data. + * @param options - Options to the attestation request. + * @param context - Context for the operation. + * + * @return Response - The result of the attestation operation + */ + Response AttestPluton( + std::vector const& dataToAttest, + AttestPlutonOptions const& options = AttestPlutonOptions{}, + Azure::Core::Context const& context = Azure::Core::Context{}) const; + private: + template + Response AttestBackend( + std::vector const& dataToAttest, + std::string const& tracingName, + std::string const& attestPath, + Azure::Core::Context const& context) const; + Azure::Core::Url m_endpoint; std::string m_apiVersion; std::shared_ptr m_pipeline; diff --git a/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_models.hpp b/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_models.hpp index 2332b94384..72c7b01705 100644 --- a/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_models.hpp +++ b/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_models.hpp @@ -61,6 +61,12 @@ namespace Azure { namespace Security { namespace Attestation { namespace Models * */ AZ_ATTESTATION_DLLEXPORT static const AttestationType Tpm; + + /** + * @brief Specifies that this should apply to Pluton security processors. + * + */ + AZ_ATTESTATION_DLLEXPORT static const AttestationType Pluton; }; /** @@ -464,6 +470,15 @@ namespace Azure { namespace Security { namespace Attestation { namespace Models std::vector TpmResult; }; + /** @brief The result of a call to AttestPluton. + */ + struct PlutonAttestationResult final + { + /** @brief Attestation response data. + */ + std::vector PlutonResult; + }; + /** * @brief The PolicyModification enumeration represents the result of an attestation * policy modification. diff --git a/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_options.hpp b/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_options.hpp index 29d15bb154..62f9d1037c 100644 --- a/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_options.hpp +++ b/sdk/attestation/azure-security-attestation/inc/azure/attestation/attestation_client_options.hpp @@ -264,6 +264,12 @@ namespace Azure { namespace Security { namespace Attestation { { }; + /** @brief Parameters sent to the attestation service for the AttestPluton API. + */ + struct AttestPlutonOptions final + { + }; + /** @brief The AttestationSigningKey represents a tuple of asymmetric private cryptographic key * and X.509 certificate wrapping the public key contained in the certificate. * diff --git a/sdk/attestation/azure-security-attestation/src/attestation_administration_client.cpp b/sdk/attestation/azure-security-attestation/src/attestation_administration_client.cpp index 1eec808ad9..d8c31e731f 100644 --- a/sdk/attestation/azure-security-attestation/src/attestation_administration_client.cpp +++ b/sdk/attestation/azure-security-attestation/src/attestation_administration_client.cpp @@ -32,6 +32,7 @@ using namespace Azure::Core::_internal; const Models::AttestationType AttestationType::SgxEnclave("SgxEnclave"); const Models::AttestationType AttestationType::OpenEnclave("OpenEnclave"); const Models::AttestationType AttestationType::Tpm("Tpm"); +const Models::AttestationType AttestationType::Pluton("Pluton"); const Models::PolicyModification PolicyModification::Removed("Removed"); const Models::PolicyModification PolicyModification::Updated("Updated"); const Models::PolicyCertificateModification PolicyCertificateModification::IsAbsent("IsAbsent"); diff --git a/sdk/attestation/azure-security-attestation/src/attestation_client.cpp b/sdk/attestation/azure-security-attestation/src/attestation_client.cpp index 17a4f432ed..cb57393a1f 100644 --- a/sdk/attestation/azure-security-attestation/src/attestation_client.cpp +++ b/sdk/attestation/azure-security-attestation/src/attestation_client.cpp @@ -203,26 +203,28 @@ Azure::Response> AttestationClient::AttestOp } } -Azure::Response AttestationClient::AttestTpm( +template +Azure::Response AttestationClient::AttestBackend( std::vector const& dataToAttest, - AttestTpmOptions const&, + std::string const& tracingName, + std::string const& attestPath, Azure::Core::Context const& context) const { - auto tracingContext(m_tracingFactory.CreateTracingContext("AttestTpm", context)); + auto tracingContext(m_tracingFactory.CreateTracingContext(tracingName, context)); try { - std::string jsonToSend = TpmDataSerializer::Serialize(dataToAttest); + std::string jsonToSend = TpmAndPlutonDataSerializer::Serialize(dataToAttest); auto encodedVector = std::vector(jsonToSend.begin(), jsonToSend.end()); Azure::Core::IO::MemoryBodyStream stream(encodedVector); auto request = AttestationCommonRequest::CreateRequest( - m_endpoint, m_apiVersion, HttpMethod::Post, {"attest/Tpm"}, &stream); + m_endpoint, m_apiVersion, HttpMethod::Post, {attestPath}, &stream); // Send the request to the service. auto response = AttestationCommonRequest::SendRequest(*m_pipeline, request, tracingContext.Context); - std::vector returnedBody{TpmDataSerializer::Deserialize(response)}; - return Response(TpmAttestationResult{returnedBody}, std::move(response)); + auto returnedBody = TpmAndPlutonDataSerializer::Deserialize(response); + return Response(ResultT{std::move(returnedBody)}, std::move(response)); } catch (std::runtime_error const& ex) { @@ -231,6 +233,23 @@ Azure::Response AttestationClient::AttestTpm( } } +Azure::Response AttestationClient::AttestTpm( + std::vector const& dataToAttest, + AttestTpmOptions const&, + Azure::Core::Context const& context) const +{ + return AttestBackend(dataToAttest, "AttestTpm", "attest/Tpm", context); +} + +Azure::Response AttestationClient::AttestPluton( + std::vector const& dataToAttest, + AttestPlutonOptions const&, + Azure::Core::Context const& context) const +{ + return AttestBackend( + dataToAttest, "AttestPluton", "attest/Pluton", context); +} + namespace { std::shared_timed_mutex SharedStateLock; } diff --git a/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.cpp b/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.cpp index 5a3c77726c..86d529ebfa 100644 --- a/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.cpp +++ b/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.cpp @@ -396,13 +396,13 @@ namespace Azure { namespace Security { namespace Attestation { namespace _detail returnValue.CertificateThumbprint, jsonResult, "x-ms-certificate-thumbprint"); return returnValue; } - std::string TpmDataSerializer::Serialize(std::vector const& tpmData) + std::string TpmAndPlutonDataSerializer::Serialize(std::vector const& data) { Azure::Core::Json::_internal::json jsonData; - jsonData["data"] = Azure::Core::_internal::Base64Url::Base64UrlEncode(tpmData); + jsonData["data"] = Azure::Core::_internal::Base64Url::Base64UrlEncode(data); return jsonData.dump(); } - std::vector TpmDataSerializer::Deserialize( + std::vector TpmAndPlutonDataSerializer::Deserialize( Azure::Core::Json::_internal::json const& jsonData) { std::vector returnValue; @@ -410,10 +410,10 @@ namespace Azure { namespace Security { namespace Attestation { namespace _detail returnValue, jsonData, "data", Azure::Core::_internal::Base64Url::Base64UrlDecode); return returnValue; } - std::vector TpmDataSerializer::Deserialize( + std::vector TpmAndPlutonDataSerializer::Deserialize( std::unique_ptr const& response) { - return TpmDataSerializer::Deserialize( + return TpmAndPlutonDataSerializer::Deserialize( Azure::Core::Json::_internal::json::parse(response->GetBody())); } diff --git a/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.hpp b/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.hpp index ed30ac1dd7..8b407257da 100644 --- a/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.hpp +++ b/sdk/attestation/azure-security-attestation/src/private/attestation_deserializers_private.hpp @@ -136,9 +136,9 @@ namespace Azure { namespace Security { namespace Attestation { namespace _detail Azure::Core::Json::_internal::json const& json); }; - struct TpmDataSerializer + struct TpmAndPlutonDataSerializer { - static std::string Serialize(std::vector const& tpmData); + static std::string Serialize(std::vector const& data); static std::vector Deserialize(Azure::Core::Json::_internal::json const& jsonData); static std::vector Deserialize( std::unique_ptr const& response); diff --git a/sdk/attestation/azure-security-attestation/test/ut/CMakeLists.txt b/sdk/attestation/azure-security-attestation/test/ut/CMakeLists.txt index b81ec3d389..8fb5220fc9 100644 --- a/sdk/attestation/azure-security-attestation/test/ut/CMakeLists.txt +++ b/sdk/attestation/azure-security-attestation/test/ut/CMakeLists.txt @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -#cspell: words tpmattestation +#cspell: words tpmattestation plutonattestation cmake_minimum_required (VERSION 3.13) project (azure-security-attestation-test LANGUAGES CXX) @@ -35,6 +35,7 @@ add_executable ( policygetset_test.cpp token_test.cpp tpmattestation_test.cpp + plutonattestation_test.cpp ) target_compile_definitions(azure-security-attestation-test PRIVATE _azure_BUILDING_TESTS) diff --git a/sdk/attestation/azure-security-attestation/test/ut/plutonattestation_test.cpp b/sdk/attestation/azure-security-attestation/test/ut/plutonattestation_test.cpp new file mode 100644 index 0000000000..8c1e62a225 --- /dev/null +++ b/sdk/attestation/azure-security-attestation/test/ut/plutonattestation_test.cpp @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "attestation_collateral.hpp" +#include "azure/attestation/attestation_administration_client.hpp" +#include "azure/attestation/attestation_client.hpp" + +#include +#include +#include + +#include + +#include + +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 PlutonInstanceType + { + Shared, + AAD, + Isolated + }; + + // cspell: words plutonattestation + static const std::string PlutonApiVersion = "2026-03-11-preview"; + + class PlutonAttestationTests : public Azure::Core::Test::TestBase { + public: + PlutonAttestationTests() { TestBase::SetUpTestSuiteLocal(AZURE_TEST_ASSETS_DIR); }; + + protected: + std::shared_ptr m_credential; + std::unique_ptr m_adminClient; + + // Create + virtual void SetUp() override + { + Azure::Core::Test::TestBase::SetUpTestBase(AZURE_TEST_RECORDING_DIR); + { + if (m_testContext.GetTestMode() != Azure::Core::Test::TestMode::PLAYBACK) + { + m_adminClient = std::make_unique( + CreateAdminClient(PlutonInstanceType::AAD)); + + // Set a minimal policy for Pluton attestation. + m_adminClient->SetAttestationPolicy( + AttestationType::Pluton, + "version=1.0; authorizationrules{=> permit();}; issuancerules{};"); + } + } + } + + virtual void TearDown() override + { + if (m_testContext.GetTestMode() != Azure::Core::Test::TestMode::PLAYBACK) + { + if (m_adminClient) + { + m_adminClient->ResetAttestationPolicy(AttestationType::Pluton); + } + } + + // Make sure you call the base classes TearDown method to ensure recordings are made. + TestBase::TearDown(); + } + + std::string GetInstanceUri(PlutonInstanceType instanceType) + { + if (instanceType == PlutonInstanceType::Shared) + { + std::string shortLocation(GetEnv("LOCATION_SHORT_NAME")); + return "https://shared" + shortLocation + "." + shortLocation + ".attest.azure.net"; + } + else if (instanceType == PlutonInstanceType::AAD) + { + return GetEnv("ATTESTATION_AAD_URL"); + } + else if (instanceType == PlutonInstanceType::Isolated) + { + return GetEnv("ATTESTATION_ISOLATED_URL"); + } + throw std::runtime_error("Unkown instance type."); + } + + 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(PlutonInstanceType instanceType) + { + // `InitClientOptions` takes care of setting up Record&Playback. + AttestationClientOptions options = InitClientOptions(); + options.ApiVersion = PlutonApiVersion; + options.TokenValidationOptions = GetTokenValidationOptions(); + auto credential = GetTestCredential(); + return AttestationClient::Create(GetInstanceUri(instanceType), credential, options); + } + + AttestationAdministrationClient CreateAdminClient(PlutonInstanceType instanceType) + { + // `InitTestClient` takes care of setting up Record&Playback. + AttestationAdministrationClientOptions options + = InitClientOptions(); + options.ApiVersion = PlutonApiVersion; + options.TokenValidationOptions = GetTokenValidationOptions(); + auto credential = GetTestCredential(); + return AttestationAdministrationClient::Create( + GetInstanceUri(instanceType), credential, options); + } + }; + + TEST_F(PlutonAttestationTests, AttestPluton_LIVEONLY_) + { + auto client(CreateClient(PlutonInstanceType::AAD)); + + std::string plutonPayload = R"({"payload": { "type": "pluton" } })"; + auto response( + client.AttestPluton(std::vector(plutonPayload.begin(), plutonPayload.end()))); + + // Verify the response contains bytes. + EXPECT_FALSE(response.Value.PlutonResult.empty()); + + // Parse the response to verify it's valid JSON with expected structure. + Azure::Core::Json::_internal::json parsedResponse( + Azure::Core::Json::_internal::json::parse(response.Value.PlutonResult)); + EXPECT_TRUE(parsedResponse.contains("payload")); + EXPECT_TRUE(parsedResponse["payload"].is_object()); + EXPECT_TRUE(parsedResponse["payload"].contains("challenge")); + EXPECT_TRUE(parsedResponse["payload"].contains("service_context")); + } + +}}}} // namespace Azure::Security::Attestation::Test diff --git a/sdk/attestation/azure-security-attestation/test/ut/policygetset_test.cpp b/sdk/attestation/azure-security-attestation/test/ut/policygetset_test.cpp index 562f8f8931..c6c662e2ad 100644 --- a/sdk/attestation/azure-security-attestation/test/ut/policygetset_test.cpp +++ b/sdk/attestation/azure-security-attestation/test/ut/policygetset_test.cpp @@ -94,6 +94,11 @@ namespace Azure { namespace Security { namespace Attestation { namespace Test { = InitClientOptions(); options.TokenValidationOptions = GetTokenValidationOptions(); + if (GetParam().TeeType == AttestationType::Pluton) + { + options.ApiVersion = "2026-03-11-preview"; + } + auto credential = GetTestCredential(); return AttestationAdministrationClient::Create(m_endpoint, credential, options); @@ -207,8 +212,8 @@ namespace Azure { namespace Security { namespace Attestation { namespace Test { * that the returned policy makes sense as an attestation policy (starts with the text "version" * - beyond that, we can't verify the response). * - * Note that VSM/VBS/TPM policies can be empty, so if we encounter an empty policy, verify that - * the policy came from TPM attestation. + * Note that VSM/VBS/TPM and Pluton policies can be empty, so if we encounter an empty policy, + * verify that the policy came from TPM or Pluton attestation. * * One additional check is performed in live mode: We verify that the issuer of the returned * attestation token matches the endpoint. This check cannot be run against recorded collateral @@ -228,7 +233,9 @@ namespace Azure { namespace Security { namespace Attestation { namespace Test { // documents. if (policy.Value.Body.empty()) { - EXPECT_EQ(AttestationType::Tpm, attestationType); + EXPECT_TRUE( + AttestationType::Tpm == attestationType + || AttestationType::Pluton == attestationType); } else { @@ -342,7 +349,10 @@ namespace Azure { namespace Security { namespace Attestation { namespace Test { for (auto const& value : typeNameList) { for (auto const& type : - {AttestationType::SgxEnclave, AttestationType::OpenEnclave, AttestationType::Tpm}) + {AttestationType::SgxEnclave, + AttestationType::OpenEnclave, + AttestationType::Tpm, + AttestationType::Pluton}) { returnCases.emplace_back(PolicyTestParam{testCaseType, value, type}); }