66using System . Linq ;
77using System . Net ;
88using System . Net . Http ;
9+ using System . Threading ;
910using System . Threading . Tasks ;
1011using Microsoft . Identity . Client . Core ;
1112using Microsoft . Identity . Client . Http ;
@@ -31,7 +32,7 @@ public static async Task<CsrMetadata> GetCsrMetadataAsync(
3132 bool probeMode )
3233 {
3334#if NET462
34- requestContext . Logger . Info ( ( ) => "[Managed Identity] IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform. Skipping IMDSv2 probe." ) ;
35+ requestContext . Logger . Info ( "[Managed Identity] IMDSv2 flow is not supported on .NET Framework 4.6.2. Cryptographic operations required for managed identity authentication are unavailable on this platform. Skipping IMDSv2 probe." ) ;
3536 return await Task . FromResult < CsrMetadata > ( null ) . ConfigureAwait ( false ) ;
3637#else
3738 var queryParams = ImdsV2QueryParamsHelper ( requestContext ) ;
@@ -66,7 +67,7 @@ public static async Task<CsrMetadata> GetCsrMetadataAsync(
6667 {
6768 if ( probeMode )
6869 {
69- requestContext . Logger . Info ( ( ) => $ "[Managed Identity] IMDSv2 CSR endpoint failure. Exception occurred while sending request to CSR metadata endpoint: $ { ex } ") ;
70+ requestContext . Logger . Info ( $ "[Managed Identity] IMDSv2 CSR endpoint failure. Exception occurred while sending request to CSR metadata endpoint: { ex } ") ;
7071 return null ;
7172 }
7273 else
@@ -187,7 +188,11 @@ internal ImdsV2ManagedIdentitySource(RequestContext requestContext) :
187188 base ( requestContext , ManagedIdentitySource . ImdsV2 )
188189 { }
189190
190- private async Task < CertificateRequestResponse > ExecuteCertificateRequestAsync ( string csr )
191+ private async Task < CertificateRequestResponse > ExecuteCertificateRequestAsync (
192+ string clientId ,
193+ string attestationEndpoint ,
194+ string csr ,
195+ ManagedIdentityKeyInfo managedIdentityKeyInfo )
191196 {
192197 var queryParams = ImdsV2QueryParamsHelper ( _requestContext ) ;
193198
@@ -199,10 +204,32 @@ private async Task<CertificateRequestResponse> ExecuteCertificateRequestAsync(st
199204 { OAuth2Header . XMsCorrelationId , _requestContext . CorrelationId . ToString ( ) }
200205 } ;
201206
207+ if ( _isMtlsPopRequested && managedIdentityKeyInfo . Type != ManagedIdentityKeyType . KeyGuard )
208+ {
209+ throw new MsalClientException (
210+ "mtls_pop_requires_keyguard" ,
211+ "[ImdsV2] mTLS Proof-of-Possession requires a KeyGuard-backed key. Enable KeyGuard or use a KeyGuard-supported environment." ) ;
212+ }
213+
214+ // TODO: : Normalize and validate attestation endpoint Code needs to be removed
215+ // once IMDS team start returning full URI
216+ Uri normalizedEndpoint = NormalizeAttestationEndpoint ( attestationEndpoint , _requestContext . Logger ) ;
217+
218+ // Ask helper for JWT only for KeyGuard keys
219+ string attestationJwt = string . Empty ;
220+ if ( managedIdentityKeyInfo . Type == ManagedIdentityKeyType . KeyGuard )
221+ {
222+ attestationJwt = await GetAttestationJwtAsync (
223+ clientId ,
224+ normalizedEndpoint ,
225+ managedIdentityKeyInfo ,
226+ _requestContext . UserCancellationToken ) . ConfigureAwait ( false ) ;
227+ }
228+
202229 var certificateRequestBody = new CertificateRequestBody ( )
203230 {
204231 Csr = csr ,
205- // AttestationToken = "fake_attestation_token" TODO: implement attestation token
232+ AttestationToken = attestationJwt
206233 } ;
207234
208235 string body = JsonHelper . SerializeToJson ( certificateRequestBody ) ;
@@ -257,12 +284,21 @@ protected override async Task<ManagedIdentityRequest> CreateRequestAsync(string
257284 {
258285 var csrMetadata = await GetCsrMetadataAsync ( _requestContext , false ) . ConfigureAwait ( false ) ;
259286
260- var keyInfo = await _requestContext . ServiceBundle . PlatformProxy . ManagedIdentityKeyProvider
261- . GetOrCreateKeyAsync ( _requestContext . Logger , _requestContext . UserCancellationToken ) . ConfigureAwait ( false ) ;
287+ IManagedIdentityKeyProvider keyProvider = _requestContext . ServiceBundle . PlatformProxy . ManagedIdentityKeyProvider ;
288+
289+ ManagedIdentityKeyInfo keyInfo = await keyProvider
290+ . GetOrCreateKeyAsync (
291+ _requestContext . Logger ,
292+ _requestContext . UserCancellationToken )
293+ . ConfigureAwait ( false ) ;
262294
263295 var ( csr , privateKey ) = _requestContext . ServiceBundle . Config . CsrFactory . Generate ( keyInfo . Key , csrMetadata . ClientId , csrMetadata . TenantId , csrMetadata . CuId ) ;
264296
265- var certificateRequestResponse = await ExecuteCertificateRequestAsync ( csr ) . ConfigureAwait ( false ) ;
297+ var certificateRequestResponse = await ExecuteCertificateRequestAsync (
298+ csrMetadata . ClientId ,
299+ csrMetadata . AttestationEndpoint ,
300+ csr ,
301+ keyInfo ) . ConfigureAwait ( false ) ;
266302
267303 // transform certificateRequestResponse.Certificate to x509 with private key
268304 var mtlsCertificate = CommonCryptographyManager . AttachPrivateKeyToCert (
@@ -302,12 +338,117 @@ private static string ImdsV2QueryParamsHelper(RequestContext requestContext)
302338 requestContext . ServiceBundle . Config . ManagedIdentityId . IdType ,
303339 requestContext . ServiceBundle . Config . ManagedIdentityId . UserAssignedId ,
304340 requestContext . Logger ) ;
341+
305342 if ( userAssignedIdQueryParam != null )
306343 {
307344 queryParams += $ "&{ userAssignedIdQueryParam . Value . Key } ={ userAssignedIdQueryParam . Value . Value } ";
308345 }
309346
310347 return queryParams ;
311348 }
349+
350+ /// <summary>
351+ /// Obtains an attestation JWT for the KeyGuard/CSR payload using the configured
352+ /// attestation provider and normalized endpoint.
353+ /// </summary>
354+ /// <param name="clientId">Client ID to be sent to the attestation provider.</param>
355+ /// <param name="attestationEndpoint">The attestation endpoint.</param>
356+ /// <param name="keyInfo">The key information.</param>
357+ /// <param name="cancellationToken">Cancellation token.</param>
358+ /// <returns>JWT string suitable for the IMDSv2 attested POP flow.</returns>
359+ /// <exception cref="MsalClientException">Wraps client/network failures.</exception>
360+
361+ private async Task < string > GetAttestationJwtAsync (
362+ string clientId ,
363+ Uri attestationEndpoint ,
364+ ManagedIdentityKeyInfo keyInfo ,
365+ CancellationToken cancellationToken )
366+ {
367+ // Provider is a local dependency; missing provider is a client error
368+ var provider = _requestContext . AttestationTokenProvider ;
369+
370+ // KeyGuard requires RSACng on Windows
371+ if ( keyInfo . Type == ManagedIdentityKeyType . KeyGuard &&
372+ keyInfo . Key is not System . Security . Cryptography . RSACng rsaCng )
373+ {
374+ throw new MsalClientException (
375+ "keyguard_requires_cng" ,
376+ "[ImdsV2] KeyGuard attestation currently supports only RSA CNG keys on Windows." ) ;
377+ }
378+
379+ // Attestation token input
380+ var input = new AttestationTokenInput
381+ {
382+ ClientId = clientId ,
383+ AttestationEndpoint = attestationEndpoint ,
384+ KeyHandle = ( keyInfo . Key as System . Security . Cryptography . RSACng ) ? . Key . Handle
385+ } ;
386+
387+ // response from provider
388+ var response = await provider ( input , cancellationToken ) . ConfigureAwait ( false ) ;
389+
390+ // Validate response
391+ if ( response == null || string . IsNullOrWhiteSpace ( response . AttestationToken ) )
392+ {
393+ throw new MsalClientException (
394+ "attestation_failed" ,
395+ "[ImdsV2] Attestation provider failed to return an attestation token." ) ;
396+ }
397+
398+ // Return the JWT
399+ return response . AttestationToken ;
400+ }
401+
402+ //To-do : Remove this method once IMDS team start returning full URI
403+ /// <summary>
404+ /// Temporarily normalize attestation endpoint values to a full https:// URI.
405+ /// IMDS team will eventually return a full URI.
406+ /// </summary>
407+ /// <param name="rawEndpoint"></param>
408+ /// <param name="logger"></param>
409+ /// <returns></returns>
410+ private static Uri NormalizeAttestationEndpoint ( string rawEndpoint , ILoggerAdapter logger )
411+ {
412+ if ( string . IsNullOrWhiteSpace ( rawEndpoint ) )
413+ {
414+ return null ;
415+ }
416+
417+ // Trim whitespace
418+ rawEndpoint = rawEndpoint . Trim ( ) ;
419+
420+ // If it already parses as an absolute URI with https, keep it.
421+ if ( Uri . TryCreate ( rawEndpoint , UriKind . Absolute , out var absolute ) &&
422+ ( absolute . Scheme . Equals ( "https" , StringComparison . OrdinalIgnoreCase ) ) )
423+ {
424+ return absolute ;
425+ }
426+
427+ // If it has no scheme (common service behavior returning only host)
428+ // prepend https:// and try again.
429+ if ( ! rawEndpoint . StartsWith ( "https://" , StringComparison . OrdinalIgnoreCase ) )
430+ {
431+ var candidate = "https://" + rawEndpoint ;
432+ if ( Uri . TryCreate ( candidate , UriKind . Absolute , out var httpsUri ) )
433+ {
434+ logger . Info ( ( ) => $ "[Managed Identity] Normalized attestation endpoint '{ rawEndpoint } ' -> '{ httpsUri . ToString ( ) } '.") ;
435+ return httpsUri ;
436+ }
437+ }
438+
439+ // Final attempt: reject http (non‑TLS) or malformed
440+ if ( Uri . TryCreate ( rawEndpoint , UriKind . Absolute , out var anyUri ) )
441+ {
442+ if ( ! anyUri . Scheme . Equals ( "https" , StringComparison . OrdinalIgnoreCase ) )
443+ {
444+ logger . Warning ( $ "[Managed Identity] Attestation endpoint uses unsupported scheme '{ anyUri . Scheme } '. HTTPS is required.") ;
445+ return null ;
446+ }
447+ return anyUri ;
448+ }
449+
450+ logger . Warning ( $ "[Managed Identity] Failed to normalize attestation endpoint value '{ rawEndpoint } '.") ;
451+ return null ;
452+ }
312453 }
313454}
0 commit comments