Skip to content

KeyGuard attestation failure is silently swallowed — MSAL sends an empty attestation token to IMDS instead of throwing #6079

Description

@gladjohn

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

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

Type

Fields

No fields configured for Bug.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions