Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ public static AcquireTokenForManagedIdentityParameterBuilder WithAttestationSupp
throw new ArgumentNullException(nameof(builder));
}

builder.CommonParameters.AttestationTokenProvider = async (endpoint, keyHandle, clientId, keyId, logger, ct) =>
builder.CommonParameters.AttestationTokenProvider = async (endpoint, keyHandle, clientIdMetadata, keyId, logger, ct) =>
{
AttestationResult result = await PopKeyAttestor.AttestCredentialGuardAsync(
endpoint,
keyHandle,
clientId,
clientIdMetadata,
keyId,
logger,
ct).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ internal static class PopKeyAttestor
// ephemeral keys. Callers always supply a non-empty keyId; null/empty is
// treated as a defensive fallback that bypasses the cache.
//
// clientId – intentionally NOT included. MAA attests the CNG key itself; clientId is
// forwarded to the service as metadata but does not change which attestation
// token is valid for a given key. Different managed identities (SAMI vs UAMI)
// sharing the same CNG key at the same endpoint correctly share the cached token.
// clientId – intentionally NOT included. MAA attests the CNG key itself; the
// client_id value is forwarded to the service as metadata but does not change
// which attestation token is valid for a given key. Different managed identities
// (SAMI vs UAMI) sharing the same CNG key at the same endpoint correctly share
// the cached token.
//
// Why a dictionary and not a single field?
// A process could theoretically use multiple MAA endpoints (e.g. in multi-region
Expand Down Expand Up @@ -96,7 +97,7 @@ internal static void ResetCacheForTest()
/// </summary>
/// <param name="endpoint">Attestation service endpoint (required).</param>
/// <param name="keyHandle">Valid SafeNCryptKeyHandle (must remain valid for duration of call).</param>
/// <param name="clientId">Optional client identifier (may be null/empty).</param>
/// <param name="clientId">Optional value forwarded as MAA <c>client_id</c> metadata (may be null/empty).</param>
/// <param name="keyId">CNG key identifier scoping the cache entry. Callers should pass the
/// CNG key's <see cref="System.Security.Cryptography.CngKey.KeyName"/> when available, or a
/// stable derived identifier (e.g. SHA-256 fingerprint of the RSA public key) for ephemeral keys.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ internal class AcquireTokenCommonParameters
/// Optional delegate for obtaining attestation JWT for Credential Guard keys.
/// Set by the KeyAttestation package via .WithAttestationSupport().
/// Returns null for non-attested flows.
/// Signature: (endpoint, keyHandle, clientId, keyId, logger, cancellationToken) → JWT or null.
/// Signature: (endpoint, keyHandle, clientIdMetadata, keyId, logger, cancellationToken) → JWT or null.
/// </summary>
public Func<string, SafeHandle, string, string, ILoggerAdapter, CancellationToken, Task<string>> AttestationTokenProvider { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class AcquireTokenForManagedIdentityParameters : IAcquireTokenParameter
/// <summary>
/// Optional delegate for obtaining attestation JWT for Credential Guard keys.
/// Set by the KeyAttestation package via .WithAttestationSupport().
/// Signature: (endpoint, keyHandle, clientId, keyId, logger, cancellationToken) → JWT or null.
/// Signature: (endpoint, keyHandle, clientIdMetadata, keyId, logger, cancellationToken) → JWT or null.
/// </summary>
public Func<string, SafeHandle, string, string, ILoggerAdapter, CancellationToken, Task<string>> AttestationTokenProvider { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ internal static bool IsSchannelFailure(Exception ex)
}

private async Task<CertificateRequestResponse> ExecuteCertificateRequestAsync(
string clientId,
string tenantId,
string attestationEndpoint,
string csr,
ManagedIdentityKeyInfo managedIdentityKeyInfo)
Expand All @@ -236,7 +236,7 @@ private async Task<CertificateRequestResponse> ExecuteCertificateRequestAsync(
if (managedIdentityKeyInfo.Type == ManagedIdentityKeyType.KeyGuard)
{
attestationJwt = await GetAttestationJwtAsync(
clientId,
tenantId,
attestationUri,
managedIdentityKeyInfo,
_requestContext.UserCancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -362,7 +362,7 @@ private async Task<MtlsBindingInfo> AcquireMtlsBindingAsync()
var privateKey = csrAndKey.privateKey;

var certificateRequestResponse = await ExecuteCertificateRequestAsync(
csrMetadata.ClientId,
csrMetadata.TenantId,
csrMetadata.AttestationEndpoint,
csr,
keyInfo).ConfigureAwait(false);
Expand Down Expand Up @@ -431,13 +431,13 @@ public async Task<MtlsBindingInfo> AcquireMtlsBindingForDelegationAsync(
/// Obtains an attestation JWT for the Credential Guard/CSR payload using the configured
/// attestation token provider delegate.
/// </summary>
/// <param name="clientId">Client ID to be sent to the attestation provider.</param>
/// <param name="tenantId">Tenant ID sent as the MAA <c>client_id</c> telemetry metadata.</param>
/// <param name="attestationEndpoint">The attestation endpoint.</param>
/// <param name="keyInfo">The key information.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>JWT string suitable for the IMDSv2 attested PoP flow, or null for non-attested flow.</returns>
private async Task<string> GetAttestationJwtAsync(
string clientId,
string tenantId,
Uri attestationEndpoint,
ManagedIdentityKeyInfo keyInfo,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -468,7 +468,7 @@ private async Task<string> GetAttestationJwtAsync(
string attestationJwt = await _attestationTokenProvider(
attestationEndpoint.AbsoluteUri,
rsaCng.Key.Handle,
clientId,
tenantId,
keyId,
_requestContext.Logger,
cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2261,6 +2261,44 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
}
}

[TestMethod]
public async Task AttestationProvider_ReceivesTenantIdAsClientIdMetadata()
{
// Arrange
string capturedClientIdMetadata = null;
PopKeyAttestor.s_testAttestationProvider = (endpoint, keyHandle, clientIdMetadata, keyId, ct) =>
{
capturedClientIdMetadata = clientIdMetadata;
const string fakeJwt = "fake.jwt.token";
var token = new AttestationToken(fakeJwt, DateTimeOffset.UtcNow.AddHours(1));
return Task.FromResult(new AttestationResult(AttestationStatus.Success, token, fakeJwt, 0, string.Empty));
};

using (new EnvVariableContext())
using (var httpManager = new MockHttpManager())
{
ManagedIdentityClient.ResetSourceForTest();
ImdsV2ManagedIdentitySource.ResetCertCacheForTest();
SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint);

var managedIdentityApp = await CreateManagedIdentityAsync(
httpManager,
managedIdentityKeyType: ManagedIdentityKeyType.KeyGuard).ConfigureAwait(false);

AddMocksToGetEntraToken(httpManager);

// Act
await managedIdentityApp.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
.WithMtlsProofOfPossession()
.WithAttestationSupport()
.ExecuteAsync().ConfigureAwait(false);

// Assert
Assert.AreEqual(TestConstants.TenantId, capturedClientIdMetadata);
Assert.AreNotEqual(TestConstants.ClientId, capturedClientIdMetadata);
}
}

[TestMethod]
public async Task MaaTokenCache_Hit_DoesNotCallAttestationProviderAgain()
{
Expand Down Expand Up @@ -2330,6 +2368,53 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource)
}
}

[TestMethod]
public async Task MaaTokenCache_DifferentClientIdMetadata_SharesCacheEntry()
{
// Arrange
int providerCallCount = 0;
string providerClientIdMetadata = null;
PopKeyAttestor.s_testAttestationProvider = (endpoint, keyHandle, clientIdMetadata, keyId, ct) =>
{
Interlocked.Increment(ref providerCallCount);
providerClientIdMetadata = clientIdMetadata;
const string fakeJwt = "fake.jwt.token";
var token = new AttestationToken(fakeJwt, DateTimeOffset.UtcNow.AddHours(1));
return Task.FromResult(new AttestationResult(AttestationStatus.Success, token, fakeJwt, 0, string.Empty));
};

const string endpoint = "https://eastus.attestation.azure.net";
const string keyId = "TestKeyClientIdMetadata";
const string managedIdentityClientId = "11111111-1111-1111-1111-111111111111";
const string tenantId = "22222222-2222-2222-2222-222222222222";

using var rsa = new RSACng(2048);
var keyHandle = rsa.Key.Handle;

// Act
AttestationResult firstResult = await PopKeyAttestor.AttestCredentialGuardAsync(
endpoint,
keyHandle,
managedIdentityClientId,
keyId,
cancellationToken: CancellationToken.None).ConfigureAwait(false);

Assert.AreEqual(1, providerCallCount);
Assert.AreEqual(managedIdentityClientId, providerClientIdMetadata);

AttestationResult secondResult = await PopKeyAttestor.AttestCredentialGuardAsync(
endpoint,
keyHandle,
tenantId,
keyId,
cancellationToken: CancellationToken.None).ConfigureAwait(false);

// Assert
Assert.AreEqual(1, providerCallCount, "Changing client_id metadata must not create a second MAA cache entry.");
Assert.AreEqual(managedIdentityClientId, providerClientIdMetadata, "The provider must not be called on the cache hit.");
Assert.AreEqual(firstResult.Jwt, secondResult.Jwt);
}

[TestMethod]
public async Task MaaTokenCache_ExpiredToken_CallsAttestationProviderAgain()
{
Expand Down
Loading