Library version
Microsoft.Identity.Client (latest 4.x) + Microsoft.Identity.Client.KeyAttestation (preview), IMDSv2 mTLS PoP flow.
Scenario
Managed Identity → AcquireTokenForManagedIdentity → .WithMtlsProofOfPossession().WithAttestationSupport() on a Confidential VM (CVM) with a KeyGuard (VBS) key.
Expected behavior
When Credential Guard / KeyGuard attestation against the MAA endpoint fails, MSAL should throw and propagate the failure reason back to the caller.
Actual behavior
The attestation failure is swallowed and converted into a non-attested request: MSAL sends an empty/omitted attestation_token to the IMDS issuecredential endpoint. The real reason (an MAA 400 PolicyEvaluationError) never reaches the caller, who instead sees an unrelated downstream failure (or an unexpectedly non-attested certificate).
Real failure being masked
Manually calling the MAA endpoint returned by IMDS reproduces the underlying deny:
Attestation failed with error code:400 description:{"error":{"code":"PolicyEvaluationError",
"innererror":{"code":"PolicyValidationFailure","details":[
{"code":"PolicyFailureClaim","target":"x-ms-azurevm-dbxvalidated","value":"false"}],
"message":"A Deny claim was issued, authorization failed."}}}
MSAL drops this entirely.
Root cause
-
src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs — the attestation provider maps every non-success status to null:
// Return JWT on success, null for non-attested flow on failure
return result.Status == Attestation.AttestationStatus.Success ? result.Jwt : null;
result.ErrorMessage / result.NativeErrorCode are discarded.
-
src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs (GetAttestationJwtAsync) treats a null/empty token as a legitimate "non-attested flow" and returns null; ExecuteCertificateRequestAsync then posts an empty attestation_token. For a KeyGuard key (where attestation is mandatory) this is incorrect.
-
src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs — the native error path builds the result with ErrorMessage = null, even though AttestationErrors.Describe(rc) exists to translate the native code into a readable reason.
Impact
Security-relevant: a KeyGuard key that should be attested can silently fall back to a non-attested credential request, and operators get no diagnosable error.
Proposed fix
- Throw
MsalServiceException("attestation_failed", …) (the failure is MAA service-originated) carrying Status / NativeErrorCode / reason instead of returning null.
- In
GetAttestationJwtAsync, treat null/empty from a configured provider as a hard failure for KeyGuard keys (do not fall back to a non-attested request).
- Populate
ErrorMessage from AttestationErrors.Describe(rc) so the reason propagates.
- Keep genuine client-side preconditions (
mtls_pop_requires_keyguard, credential_guard_requires_cng) as MsalClientException.
Minimal repro
SAMI on a CVM, KeyGuard key, attestation deny (e.g. x-ms-azurevm-dbxvalidated=false): AcquireToken…WithMtlsProofOfPossession().WithAttestationSupport() succeeds non-attested / fails opaquely instead of throwing attestation_failed.
Related
Companion diagnostics issue: native AttestationClientLib (MAA) logs are not bridged into the MSAL logger, which makes this failure hard to diagnose.
Library version
Microsoft.Identity.Client(latest 4.x) +Microsoft.Identity.Client.KeyAttestation(preview), IMDSv2 mTLS PoP flow.Scenario
Managed Identity →
AcquireTokenForManagedIdentity→.WithMtlsProofOfPossession().WithAttestationSupport()on a Confidential VM (CVM) with a KeyGuard (VBS) key.Expected behavior
When Credential Guard / KeyGuard attestation against the MAA endpoint fails, MSAL should throw and propagate the failure reason back to the caller.
Actual behavior
The attestation failure is swallowed and converted into a non-attested request: MSAL sends an empty/omitted
attestation_tokento the IMDSissuecredentialendpoint. The real reason (an MAA400 PolicyEvaluationError) never reaches the caller, who instead sees an unrelated downstream failure (or an unexpectedly non-attested certificate).Real failure being masked
Manually calling the MAA endpoint returned by IMDS reproduces the underlying deny:
MSAL drops this entirely.
Root cause
src/client/Microsoft.Identity.Client.KeyAttestation/ManagedIdentityAttestationExtensions.cs— the attestation provider maps every non-success status tonull:result.ErrorMessage/result.NativeErrorCodeare discarded.src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ImdsV2ManagedIdentitySource.cs(GetAttestationJwtAsync) treats a null/empty token as a legitimate "non-attested flow" and returnsnull;ExecuteCertificateRequestAsyncthen posts an emptyattestation_token. For a KeyGuard key (where attestation is mandatory) this is incorrect.src/client/Microsoft.Identity.Client.KeyAttestation/Attestation/AttestationClient.cs— the native error path builds the result withErrorMessage = null, even thoughAttestationErrors.Describe(rc)exists to translate the native code into a readable reason.Impact
Security-relevant: a KeyGuard key that should be attested can silently fall back to a non-attested credential request, and operators get no diagnosable error.
Proposed fix
MsalServiceException("attestation_failed", …)(the failure is MAA service-originated) carryingStatus/NativeErrorCode/ reason instead of returningnull.GetAttestationJwtAsync, treat null/empty from a configured provider as a hard failure for KeyGuard keys (do not fall back to a non-attested request).ErrorMessagefromAttestationErrors.Describe(rc)so the reason propagates.mtls_pop_requires_keyguard,credential_guard_requires_cng) asMsalClientException.Minimal repro
SAMI on a CVM, KeyGuard key, attestation deny (e.g.
x-ms-azurevm-dbxvalidated=false):AcquireToken…WithMtlsProofOfPossession().WithAttestationSupport()succeeds non-attested / fails opaquely instead of throwingattestation_failed.Related
Companion diagnostics issue: native AttestationClientLib (MAA) logs are not bridged into the MSAL logger, which makes this failure hard to diagnose.