Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions sdk/attestation/azure-security-attestation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,29 @@ 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<PlutonAttestationResult> - The result of the attestation operation
*/
Response<Models::PlutonAttestationResult> AttestPluton(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the AttestPluton depends on a custom API version, document the requirement of the custom API version.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note added.

std::vector<uint8_t> const& dataToAttest,
AttestPlutonOptions const& options = AttestPlutonOptions{},
Comment thread
nguyenteresaMSFT marked this conversation as resolved.
Azure::Core::Context const& context = Azure::Core::Context{}) const;
Comment thread
nguyenteresaMSFT marked this conversation as resolved.
Comment thread
nguyenteresaMSFT marked this conversation as resolved.

private:
template <typename ResultT>
Response<ResultT> AttestBackend(
std::vector<uint8_t> 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<Azure::Core::Http::_internal::HttpPipeline> m_pipeline;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -464,6 +470,15 @@ namespace Azure { namespace Security { namespace Attestation { namespace Models
std::vector<uint8_t> TpmResult;
};

/** @brief The result of a call to AttestPluton.
*/
struct PlutonAttestationResult final
{
/** @brief Attestation response data.
*/
std::vector<uint8_t> PlutonResult;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the attestation result is unstructured data, it implies that a client cannot make any assumptions about the contents of the PlutonResult, the only thing they can depend on is the status of the operation.

That restriction may be excessively limiting to your customers, but the documentation for this API is extremely minimal.

I'm not 100% sure how any customer is going to be able to use this API.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Work to create public documentation describing the Pluton protocol messages is on our radar. Our primary goal for now is to unblock our partner team who are already acquainted with the request/response structures.

};

/**
* @brief The PolicyModification enumeration represents the result of an attestation
* policy modification.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,26 +203,28 @@ Azure::Response<AttestationToken<AttestationResult>> AttestationClient::AttestOp
}
}

Azure::Response<TpmAttestationResult> AttestationClient::AttestTpm(
template <typename ResultT>
Azure::Response<ResultT> AttestationClient::AttestBackend(
std::vector<uint8_t> 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<uint8_t>(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<uint8_t> returnedBody{TpmDataSerializer::Deserialize(response)};
return Response<TpmAttestationResult>(TpmAttestationResult{returnedBody}, std::move(response));
auto returnedBody = TpmAndPlutonDataSerializer::Deserialize(response);
return Response<ResultT>(ResultT{std::move(returnedBody)}, std::move(response));
}
catch (std::runtime_error const& ex)
{
Expand All @@ -231,6 +233,22 @@ Azure::Response<TpmAttestationResult> AttestationClient::AttestTpm(
}
}

Azure::Response<TpmAttestationResult> AttestationClient::AttestTpm(
std::vector<uint8_t> const& dataToAttest,
AttestTpmOptions const&,
Azure::Core::Context const& context) const
{
return AttestBackend<TpmAttestationResult>(dataToAttest, "AttestTpm", "attest/Tpm", context);
}

Azure::Response<PlutonAttestationResult> AttestationClient::AttestPluton(
std::vector<uint8_t> const& dataToAttest,
AttestPlutonOptions const&,
Azure::Core::Context const& context) const
{
return AttestBackend<PlutonAttestationResult>(dataToAttest, "AttestPluton", "attest/Pluton", context);
}

namespace {
std::shared_timed_mutex SharedStateLock;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,24 +396,24 @@ namespace Azure { namespace Security { namespace Attestation { namespace _detail
returnValue.CertificateThumbprint, jsonResult, "x-ms-certificate-thumbprint");
return returnValue;
}
std::string TpmDataSerializer::Serialize(std::vector<uint8_t> const& tpmData)
std::string TpmAndPlutonDataSerializer::Serialize(std::vector<uint8_t> 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<uint8_t> TpmDataSerializer::Deserialize(
std::vector<uint8_t> TpmAndPlutonDataSerializer::Deserialize(
Azure::Core::Json::_internal::json const& jsonData)
{
std::vector<uint8_t> returnValue;
JsonOptional::SetIfExists<std::string, std::vector<uint8_t>>(
returnValue, jsonData, "data", Azure::Core::_internal::Base64Url::Base64UrlDecode);
return returnValue;
}
std::vector<uint8_t> TpmDataSerializer::Deserialize(
std::vector<uint8_t> TpmAndPlutonDataSerializer::Deserialize(
std::unique_ptr<Azure::Core::Http::RawResponse> const& response)
{
return TpmDataSerializer::Deserialize(
return TpmAndPlutonDataSerializer::Deserialize(
Azure::Core::Json::_internal::json::parse(response->GetBody()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> const& tpmData);
static std::string Serialize(std::vector<uint8_t> const& data);
static std::vector<uint8_t> Deserialize(Azure::Core::Json::_internal::json const& jsonData);
static std::vector<uint8_t> Deserialize(
std::unique_ptr<Azure::Core::Http::RawResponse> const& response);
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <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 PlutonInstanceType
{
Shared,
AAD,
Isolated
};

// cspell: words plutonattestation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Piling onto my previous comment: Non-beta Azure SDKs cannot depend on preview API versions. So until the attestation service GA's the 2026-03-11 API version, the attestation client can only be in beta release.

static const std::string PlutonApiVersion = "2026-03-11-preview";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally clients don't specify the API version and instead use the current API version. It's surprising that the pluton attestation APIs require that the customer provide a unique API version.

In general, the API version construct accepted by the Azure SDKs is flawed - the idea is that you could specify an older or newer API version on individual clients but it also meant that there could be no wire differences between the inputs and outputs of the service. But the API version defines the inputs and outputs of the service and any/all breaking changes in the wire API are required to have a version bump (thus for statically typed languages like C#, C++, Rust, Java this requires source changes on the SDK). So for statically typed languages, the use of the Api Version is strongly discouraged.

Also, this locks your clients into a preview API which also means that you force the server to support a preview version forever (at least a decade). The Attestation service currently has to support the Beta1 preview because a partner team shipped client software that depended on a preview version. You really don't want to go there.

Is there a reason you can't bump the required API version for the SDK?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thought was to only expose the Pluton endpoint through a preview version because our partner team is still integrating the client component, and this SDK update is meant to unblock them. Is it preferable to have them use a preview version while they are still in development and then GA later on before shipping, or simply release 2026-03-11 as a stable API version from the start? Olga Kroshkina (@olkroshk) , Greg Kostal (@gkostal) , what do you folks think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the bottom line: Having a public release of an Azure SDK that requires a preview API version is a violation of our guidelines on API versioning. So unfortunately we can't allow a public release of an SDK client that depends on a -preview API version for any of its functionality :(.

However there's nothing stopping you from having as many beta releases as you want for your partner teams to test. But you do need to know that beta releases go into the beta package repository, so your partner teams either need to use the beta registry or they need to use a git submodule (or the moral equivalent of a git submodule) to get access to your SDK.

And honestly that's a good thing - keep the Pluton APIs with a -preview API version until you've had validation from your partner teams and then once you release the GA version, update the client TSP file with the release version and update the released version of the SDK.

And once that is done, update the SDK to require the current API version for all the APIs (I noticed there have been a few stable releases of the attestation swagger/tsp files so it's probably past time to align the TSP files and the SDKs.

** OFFTOPIC **
Oh, and while I have Greg and Olga on the line: You might want to take a stab at generating the Rust SDK for attestation - IMHO the attestation TSP file doesn't do a really good job of expressing the desired API surface of the SDK, which you can REALLY see when you look at the Rust SDK (The Rust SDKs are supposed to all be generated directly from the TSP files).


class PlutonAttestationTests : public Azure::Core::Test::TestBase {
public:
PlutonAttestationTests() { TestBase::SetUpTestSuiteLocal(AZURE_TEST_ASSETS_DIR); };

protected:
std::shared_ptr<const Azure::Core::Credentials::TokenCredential> m_credential;
std::unique_ptr<AttestationAdministrationClient> 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<AttestationAdministrationClient>(
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<AttestationClientOptions>();
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<AttestationAdministrationClientOptions>();
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<uint8_t>(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
Loading
Loading