+ uVzRLpN3CBAoZGXdj/25Y1GlajVYeJm13Vh2AIG3SuPFOqxtBS6tTQH0p2TtBD2yvGaOKbEYdpQXKltvf1rCoh1BBy5C8hSPn6xCH3PMLJHflsZCny/BudrkaWEAhvOxSL/LmQQZQxu5A3Tld+glJCtTfldnx25BIMfmH1oOvqS4tykMej3/29mKiw9+oY/Fka1WMy+sl1VAYskXfnEPcuVfzEbaIl9Ur/FhBWLcyBtE1BovaUe1loYBzUJOgxQ87rTFetmRATTyEL3TthMFxq9RVCDi+s5o+mwKz4WPoQlxqwAMAl6HWYYGfeQcU8LEFrYwoWtoU+nnfM5qBO9hXQ==
+ AQAB
+ 3pGBJXfhILNTsbRLHmUy7YVvD75HpvMCey2aaN4gU9Jvi1s2vQFU15a8p75Yt8UYHZDr+Yqwl1Jd4J+UtWsGqGBGNB1Ae4V1dwR8zUDKxXXee7G/dCDnIu4xpkZbPD+brcULcpF/Tdq/WsTbpCNhPgjHuo8hQY3vFv1NMla8mr0=
+ 1TSgE9DfTeqk0qybQM1r83M5ZwWKV0mPQBZl1VMs+VplB6E/6JAYWCKiq9ewgocOaktK94jtEtsaDhYeyojZFBlukt1lKp4kmkUwUSEmi3EFsprNakg+Bm6t85tEm5he5mG1ivHlE3M5lBWJ2A0r1g3jWSjYJlkk2nOwFE8bmyE=
+ UIcU0xmsusgnYAR7qWO0KXw90tRl2GHUY/z8ATVdPPbGpQU7qObya45+c7LLJrKJJyloN8GWYynKDZuvknRG1GUBAZoT2p1PAuD8xsbKlucuuFJ3kuzUtC66iA6ss//Ps++3VJyQEvsygQT480pZxLgoi7d9sNpJx2eeprf7RYE=
+ zwIZqyPSrUR2ZFdTJshNWEM4KN8oQzgY7pDQrx/jOviZv57A/n1qJaj7aP4zU4juZiZU06MPDI/P7H1tyBi3LNzEj7SG1apWv7MOBre5RQqoDZJggCFEl9o+65iGNMzs16NnMVFMqmXmMfH3tN6VAXDanWca96D2N2S8QfvNQgE=
+ Uoxh1dskd3C0N7SQ1nJXW7FyjB+J54R5yAcd8Zk0ukunhtuzsziQH4ZoMhBuzwxRwOaw0Umj77EcdEevuvFHn6LAK/solK2lkRcuKY2QTgkbYyYOxZNa1pJJaAfgzSGsBiwiGtHXl2eFLb2jfYDa4V/SV2B6BPOVheSUQGZlyYM=
+ Lkq21wnu7S2T2NbzyVUVKm+mfurJqHzCxX+lIKVEkEhn5ipPo76vew7k+bUj2C5MZ+64zEK1GFANpP9mzghtmSzzI4bzIx/tanQLo2047VyU2UO0Oaskl3TKHGMkTY+ok8GKaDF02aSfxPQ5poNsWycS1/eeLFklnLkviF7mVcfCoStSHAb+8dQzxO22Mu+oN2rXHinoNDSmFzUTx8cJapQhgji+GADRKF77Sfa5tHk/hCzVUXGBHgBs1jJM9cin2BBij8PngOaAAlby4gr07/r8SZU2uuXoxEDhpxf6mRTET5Wr2hxAyhu3bpZeCc0LokckNkzJPGUG6JaXXdUcgQ==
+";
+ #endregion
}
internal static class Adfs2019LabConstants
diff --git a/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
new file mode 100644
index 0000000000..a50fcd376b
--- /dev/null
+++ b/tests/Microsoft.Identity.Test.E2e/KeyGuardAttestationTests.cs
@@ -0,0 +1,228 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/*
+WHY THESE TESTS ONLY RUN ON A SPECIFIC (AZURE ARC) MACHINE
+---------------------------------------------------------
+KeyGuard attestation requires:
+ 1. A KeyGuard / Virtualization-Based Security (VBS) capable environment.
+ 2. The ability to create a CNG RSA key with:
+ - Virtual Isolation (NCRYPT_USE_VIRTUAL_ISOLATION_FLAG)
+ - Per-boot scope (NCRYPT_USE_PER_BOOT_KEY_FLAG)
+ 3. A native KeyGuard attestation stack (deployed via the MtlsPop package) capable of:
+ - Accessing the key handle
+ - Interacting with the VBS services to produce an attestation
+
+Most hosted build agents (including standard Azure DevOps Microsoft-hosted pools) do NOT expose:
+ - Virtualization-based key isolation
+ - The necessary kernel components for KeyGuard property retrieval
+ - The proper security context to create KeyGuard-protected keys
+
+We therefore run these tests ONLY on a dedicated Azure Arc–connected VM (custom self-hosted agent) that:
+ - Is provisioned with VBS + KeyGuard enabled
+ - Has the Microsoft Software Key Storage Provider configured to honor Virtual Isolation + per-boot flags
+ - Has an identity/endpoint (TOKEN_ATTESTATION_ENDPOINT) capable of accepting and validating a KeyGuard attestation
+ - Is allowed in the pipeline via filtering on the TestCategory MI_E2E_AzureArc (and infra chooses that agent)
+
+If any prerequisite is missing (e.g., VBS off, endpoint unset, native DLL absent, or key not actually KeyGuard-protected),
+the test exits early with Assert.Inconclusive instead of failing the overall build.
+*/
+
+using Microsoft.Identity.Client.MtlsPop.Attestation;
+using Microsoft.Identity.Test.Common.Core.Helpers;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using Microsoft.Identity.Client.MtlsPop;
+using System.Threading.Tasks;
+using System.Threading;
+
+namespace Microsoft.Identity.Test.E2E
+{
+ [TestClass]
+ public class KeyGuardAttestationTests
+ {
+ /*
+ Creates a KeyGuard-capable RSA key (2048-bit) using the Microsoft Software Key Storage Provider.
+ Flags:
+ - NCRYPT_USE_VIRTUAL_ISOLATION_FLAG: Requests KeyGuard / Virtual Isolation (backed by VBS).
+ - NCRYPT_USE_PER_BOOT_KEY_FLAG: Key material only valid for the current boot (expected scenario for attestation).
+ On machines without KeyGuard/VBS support the provider may silently ignore the flags; we detect that later via IsKeyGuardProtected.
+ IMPORTANT: This must run on the Azure Arc custom agent where VBS + KeyGuard is enabled.
+ */
+ private static CngKey CreateKeyGuardKey(string keyName)
+ {
+ const string ProviderName = "Microsoft Software Key Storage Provider";
+ const int NCRYPT_USE_VIRTUAL_ISOLATION_FLAG = 0x00020000;
+ const int NCRYPT_USE_PER_BOOT_KEY_FLAG = 0x00040000;
+
+ var p = new CngKeyCreationParameters
+ {
+ Provider = new CngProvider(ProviderName),
+ ExportPolicy = CngExportPolicies.None, // No export allowed; expected for attested keys.
+ KeyUsage = CngKeyUsages.AllUsages, // Broad usage; attestation library only needs signing.
+ KeyCreationOptions =
+ CngKeyCreationOptions.OverwriteExistingKey |
+ (CngKeyCreationOptions)NCRYPT_USE_VIRTUAL_ISOLATION_FLAG |
+ (CngKeyCreationOptions)NCRYPT_USE_PER_BOOT_KEY_FLAG,
+ };
+
+ // Set 2048-bit RSA length (current attestation native lib expects RSA; adjust only with platform guidance).
+ p.Parameters.Add(new CngProperty(
+ "Length",
+ BitConverter.GetBytes(2048),
+ CngPropertyOptions.None));
+
+ return CngKey.Create(CngAlgorithm.Rsa, keyName, p);
+ }
+
+ /*
+ Determines whether the key actually received KeyGuard Virtual Isolation backing.
+ Some environments will accept the creation flags but produce a normal (non-KeyGuard) key;
+ those runs should be marked Inconclusive rather than Fail to avoid noisy pipeline failures.
+ This mirrors the logic used in other internal tracking (ref #5448).
+ */
+ private static bool IsKeyGuardProtected(CngKey key)
+ {
+ try
+ {
+ // KeyGuard exposes a "Virtual Iso" property that is non-zero when protected.
+ // Same check used in #5448. :contentReference[oaicite:1]{index=1}
+ var prop = key.GetProperty("Virtual Iso", CngPropertyOptions.None);
+ var bytes = prop.GetValue();
+ return bytes != null && bytes.Length >= 4 && BitConverter.ToInt32(bytes, 0) != 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ /*
+ Synchronous attestation path.
+ Restricted to Azure Arc (MI_E2E_AzureArc) because:
+ - Needs a machine with KeyGuard + VBS
+ - Needs TOKEN_ATTESTATION_ENDPOINT env var (injected by pipeline/agent config)
+ - Uses AttestationClient which depends on a native DLL deployed only on that custom agent
+ Fails fast with Assert.Inconclusive when prerequisites are missing.
+ */
+ [TestCategory("MI_E2E_AzureArc")]
+ [RunOnAzureDevOps]
+ [TestMethod]
+ public void Attest_KeyGuardKey_OnAzureArc_Succeeds()
+ {
+ // Endpoint is provisioned only on the Azure Arc agent (backed by MSI / identity service).
+ var endpoint = Environment.GetEnvironmentVariable("TOKEN_ATTESTATION_ENDPOINT");
+ if (string.IsNullOrWhiteSpace(endpoint))
+ {
+ Assert.Inconclusive($"Set {"TOKEN_ATTESTATION_ENDPOINT"} on the Azure Arc agent to run this test.");
+ }
+
+ // Placeholder logical client ID used by the attestation endpoint (matches agent configuration).
+ var clientId = "MSI_CLIENT_ID";
+ string keyName = "MsalE2E_Keyguard";
+
+ CngKey key = null;
+ try
+ {
+ key = CreateKeyGuardKey(keyName);
+
+ if (!IsKeyGuardProtected(key))
+ {
+ // Indicates environment does not truly support KeyGuard (e.g., VBS disabled) — do not treat as test failure.
+ Assert.Inconclusive("Key was created but not KeyGuard-protected. Is KeyGuard/VBS enabled on this machine?");
+ }
+
+ // Use the new public AttestationClient from the MtlsPop package. :contentReference[oaicite:2]{index=2}
+ using var client = new AttestationClient();
+ var result = client.Attest(endpoint, key.Handle, clientId);
+
+ // Validate success + JWT shape (3 parts).
+ Assert.AreEqual(AttestationStatus.Success, result.Status,
+ $"Attestation failed: status={result.Status}, nativeRc={result.NativeErrorCode}, msg={result.ErrorMessage}");
+ Assert.IsFalse(string.IsNullOrEmpty(result.Jwt), "Expected a non-empty attestation JWT.");
+
+ var parts = result.Jwt.Split('.');
+ Assert.AreEqual(3, parts.Length, "Expected a JWT (3 parts).");
+ }
+ catch (CryptographicException ex)
+ {
+ // Common when provider flags unsupported or isolation services absent.
+ Assert.Inconclusive("CNG/KeyGuard is not available or access is denied on this machine: " + ex.Message);
+ }
+ catch (InvalidOperationException ex)
+ {
+ // Thrown by AttestationClient when the native DLL cannot be found/initialized (not deployed outside Azure Arc agent).
+ Assert.Inconclusive("Attestation native lib not available on this runner: " + ex.Message);
+ }
+ finally
+ {
+ try { key?.Delete(); } catch { /* best-effort cleanup */ }
+ }
+ }
+
+ /*
+ Async attestation path.
+ Demonstrates PopKeyAttestor.AttestKeyGuardAsync which wraps the native synchronous call.
+ Same environmental constraints as the synchronous test; still limited to the Azure Arc agent.
+ */
+ [TestCategory("MI_E2E_AzureArc")]
+ [RunOnAzureDevOps]
+ [TestMethod]
+ public async Task Attest_KeyGuardKey_OnAzureArc_Async_Succeeds()
+ {
+ var endpoint = Environment.GetEnvironmentVariable("TOKEN_ATTESTATION_ENDPOINT");
+ if (string.IsNullOrWhiteSpace(endpoint))
+ {
+ Assert.Inconclusive($"Set {"TOKEN_ATTESTATION_ENDPOINT"} on the Azure Arc agent to run this test.");
+ }
+
+ var clientId = "MSI_CLIENT_ID";
+ string keyName = "MsalE2E_Keyguard_Async";
+
+ CngKey key = null;
+ try
+ {
+ key = CreateKeyGuardKey(keyName);
+
+ if (!IsKeyGuardProtected(key))
+ {
+ Assert.Inconclusive("Key was created but not KeyGuard-protected. Is KeyGuard/VBS enabled on this machine?");
+ }
+
+ // Exercise the async facade (PopKeyAttestor) which wraps the synchronous native call in Task.Run.
+ var result = await PopKeyAttestor.AttestKeyGuardAsync(
+ endpoint,
+ key.Handle,
+ clientId: clientId,
+ cancellationToken: CancellationToken.None).ConfigureAwait(false);
+
+ Assert.AreEqual(AttestationStatus.Success, result.Status,
+ $"Async attestation failed: status={result.Status}, nativeRc={result.NativeErrorCode}, msg={result.ErrorMessage}");
+ Assert.IsFalse(string.IsNullOrEmpty(result.Jwt), "Expected a non-empty attestation JWT from async path.");
+
+ var parts = result.Jwt.Split('.');
+ Assert.AreEqual(3, parts.Length, "Expected a JWT (3 parts) from async path.");
+ }
+ catch (CryptographicException ex)
+ {
+ Assert.Inconclusive("CNG/KeyGuard is not available or access is denied on this machine: " + ex.Message);
+ }
+ catch (InvalidOperationException ex)
+ {
+ // Could originate from native initialization inside PopKeyAttestor (AttestationClient constructor).
+ Assert.Inconclusive("Attestation native lib not available on this runner (async path): " + ex.Message);
+ }
+ catch (ArgumentException ex)
+ {
+ // Defensive: invalid handle or parameters — treat as environment/setup issue for this scenario.
+ Assert.Inconclusive("Handle or parameters invalid for async attestation path: " + ex.Message);
+ }
+ finally
+ {
+ try { key?.Delete(); } catch { /* best-effort cleanup */ }
+ }
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs
index 1dd068de26..3b434ef2cc 100644
--- a/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs
+++ b/tests/Microsoft.Identity.Test.E2e/ManagedIdentityImdsTests.cs
@@ -3,6 +3,7 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
+using Microsoft.Identity.Client.MtlsPop;
using Microsoft.Identity.Test.Common.Core.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
@@ -37,11 +38,11 @@ private static IManagedIdentityApplication BuildMi(
[RunOnAzureDevOps]
[TestCategory("MI_E2E_Imds")]
[DataTestMethod]
- [DataRow(null /*SAMI*/, null, DisplayName = "SAMI")]
- [DataRow("4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6", "clientid", DisplayName = "UAMI-ClientId")]
+ [DataRow(null /*SAMI*/, null, DisplayName = "AcquireToken_OnImds_Succeeds-SAMI")]
+ [DataRow("4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6", "clientid", DisplayName = "AcquireToken_OnImds_Succeeds-UAMI-ClientId")]
[DataRow("/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/MSAL_MSI/providers/Microsoft.ManagedIdentity/userAssignedIdentities/LabVaultAccess_UAMI",
- "resourceid", DisplayName = "UAMI-ResourceId")]
- [DataRow("1eee55b7-168a-46be-8d19-30e830ee9611", "objectid", DisplayName = "UAMI-ObjectId")]
+ "resourceid", DisplayName = "AcquireToken_OnImds_Succeeds-UAMI-ResourceId")]
+ [DataRow("1eee55b7-168a-46be-8d19-30e830ee9611", "objectid", DisplayName = "AcquireToken_OnImds_Succeeds-UAMI-ObjectId")]
public async Task AcquireToken_OnImds_Succeeds(string id, string idType)
{
var mi = BuildMi(id, idType);
@@ -63,5 +64,26 @@ public async Task AcquireToken_OnImds_Succeeds(string id, string idType)
Assert.AreEqual(TokenSource.Cache, second.AuthenticationResultMetadata.TokenSource);
Assert.AreEqual(result.AccessToken, second.AccessToken);
}
+
+ [RunOnAzureDevOps]
+ [TestCategory("MI_E2E_Imds")]
+ [DataTestMethod]
+ [DataRow(null /*SAMI*/, null, DisplayName = "AcquireToken_OnImds_Fails_WithMtlsProofOfPossession-SAMI")]
+ [DataRow("4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6", "clientid", DisplayName = "AcquireToken_OnImds_Fails_WithMtlsProofOfPossession-UAMI-ClientId")]
+ [DataRow("/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/MSAL_MSI/providers/Microsoft.ManagedIdentity/userAssignedIdentities/LabVaultAccess_UAMI",
+ "resourceid", DisplayName = "AcquireToken_OnImds_Fails_WithMtlsProofOfPossession-UAMI-ResourceId")]
+ [DataRow("1eee55b7-168a-46be-8d19-30e830ee9611", "objectid", DisplayName = "AcquireToken_OnImds_Fails_WithMtlsProofOfPossession-UAMI-ObjectId")]
+ public async Task AcquireToken_OnImds_Fails_WithMtlsProofOfPossession(string id, string idType)
+ {
+ var mi = BuildMi(id, idType);
+
+ var ex = await Assert.ThrowsExceptionAsync