-
Notifications
You must be signed in to change notification settings - Fork 373
ImdsV2: Acquire Entra Token Over mTLS #5431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Robbie-Microsoft
merged 54 commits into
rginsburg/msiv2_feature_branch
from
rginsburg/msiv2_acquire_entra_token
Aug 29, 2025
Merged
Changes from 51 commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
5498968
Initial commit. 2 TODOs
Robbie-Microsoft e04e408
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_csr
Robbie-Microsoft 4e096b7
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_csr
Robbie-Microsoft 6bc2164
Implemented CSR generator
Robbie-Microsoft 762ccdf
first pass at improved unit tests
Robbie-Microsoft 4ea6c09
Finished improving unit tests
Robbie-Microsoft 009f948
Updates to CUID
Robbie-Microsoft 21d4ef3
Unit test improvements
Robbie-Microsoft cd013a3
Implemented Feedback
Robbie-Microsoft 480ae9e
renamed file
Robbie-Microsoft 0aa8692
small improvement
Robbie-Microsoft de24670
Initial implementation
Robbie-Microsoft 621c566
added missing awaitor for async method
Robbie-Microsoft 07b7883
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft 068461b
Fixed bugs discovered from unit testing in child branch
Robbie-Microsoft 8a8439a
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft 2034b25
undid changes to .proj
Robbie-Microsoft b415f99
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft 2b7486a
undid change to global.json
Robbie-Microsoft a76d2fd
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft 310c467
started unit testing
Robbie-Microsoft 189ff9e
added missing sets
Robbie-Microsoft a98a5a8
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft 9345e99
merged from parent branch
Robbie-Microsoft c72e61b
undid changes to global.json
Robbie-Microsoft 92b325f
Inplemented some feedback
Robbie-Microsoft 5ae0596
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft e85fc9a
merged from parent
Robbie-Microsoft 067c83c
Implemented some feedback
Robbie-Microsoft f7d6f88
PKCS1 -> Pss padding
Robbie-Microsoft 74e8e60
re-used imports
Robbie-Microsoft 152f396
Implemented feedback
Robbie-Microsoft d46c853
Changes from manual testing.
Robbie-Microsoft 3f75e3a
ImdsV2: Reworked Custom ASN1 Encoder to use System.Formats.Asn1 Nuget…
Robbie-Microsoft 253993d
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_csr
Robbie-Microsoft c035de0
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft 755cf6f
Implemented some feedback
Robbie-Microsoft 14d05f1
additional improvements
Robbie-Microsoft 9b5e498
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_ac…
Robbie-Microsoft 69e714a
fixed bad rebase
Robbie-Microsoft 4219044
Fixed bad rebase
Robbie-Microsoft 8d3c7ac
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_ac…
Robbie-Microsoft 3bab6e4
Fixed bad rebase
Robbie-Microsoft 6dacdf5
Adjusted variable names after rebase
Robbie-Microsoft 0df6e1e
wrote the skeleton for the mTLS cert creation
Robbie-Microsoft a66a933
adjusted unit test based on new code
Robbie-Microsoft 67cc4a6
Implemented mTLS
Robbie-Microsoft ca6f1d6
Removed un-used imports
Robbie-Microsoft 3fc3ece
Undo changes to global.json
Robbie-Microsoft 131b8cd
Implemented unit test + helpers
Robbie-Microsoft d839578
Undid changes to csproj
Robbie-Microsoft 7118235
Implemented feedback
Robbie-Microsoft 8fcac99
Improved unit tests. Added UAMI unit tests.
Robbie-Microsoft 59aefb3
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_ac…
Robbie-Microsoft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
src/client/Microsoft.Identity.Client/ManagedIdentity/V2/DefaultCsrFactory.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System.Security.Cryptography; | ||
|
||
namespace Microsoft.Identity.Client.ManagedIdentity.V2 | ||
{ | ||
internal class DefaultCsrFactory : ICsrFactory | ||
{ | ||
public (string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuid) | ||
{ | ||
return Csr.Generate(clientId, tenantId, cuid); | ||
} | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/client/Microsoft.Identity.Client/ManagedIdentity/V2/ICsrFactory.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System.Security.Cryptography; | ||
|
||
namespace Microsoft.Identity.Client.ManagedIdentity.V2 | ||
{ | ||
internal interface ICsrFactory | ||
{ | ||
(string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuid); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ | |
using System.Collections.Generic; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Security.Cryptography; | ||
using System.Security.Cryptography.X509Certificates; | ||
using System.Threading.Tasks; | ||
using Microsoft.Identity.Client.Core; | ||
using Microsoft.Identity.Client.Http; | ||
|
@@ -20,6 +22,7 @@ internal class ImdsV2ManagedIdentitySource : AbstractManagedIdentity | |
public const string ImdsV2ApiVersion = "2.0"; | ||
public const string CsrMetadataPath = "/metadata/identity/getplatformmetadata"; | ||
public const string CertificateRequestPath = "/metadata/identity/issuecredential"; | ||
public const string AcquireEntraTokenPath = "/oauth2/v2.0/token"; | ||
|
||
public static async Task<CsrMetadata> GetCsrMetadataAsync( | ||
RequestContext requestContext, | ||
|
@@ -251,12 +254,150 @@ private async Task<CertificateRequestResponse> ExecuteCertificateRequestAsync(st | |
protected override async Task<ManagedIdentityRequest> CreateRequestAsync(string resource) | ||
{ | ||
var csrMetadata = await GetCsrMetadataAsync(_requestContext, false).ConfigureAwait(false); | ||
var csr = Csr.Generate(csrMetadata.ClientId, csrMetadata.TenantId, csrMetadata.CuId); | ||
var (csr, privateKey) = _requestContext.ServiceBundle.Config.CsrFactory.Generate(csrMetadata.ClientId, csrMetadata.TenantId, csrMetadata.CuId); | ||
|
||
var certificateRequestResponse = await ExecuteCertificateRequestAsync(csr).ConfigureAwait(false); | ||
|
||
// transform certificateRequestResponse.Certificate to x509 with private key | ||
var mtlsCertificate = AttachPrivateKeyToCert( | ||
certificateRequestResponse.Certificate, | ||
privateKey); | ||
|
||
ManagedIdentityRequest request = new(HttpMethod.Post, new Uri($"{certificateRequestResponse.MtlsAuthenticationEndpoint}/{certificateRequestResponse.TenantId}{AcquireEntraTokenPath}")); | ||
request.Headers.Add("x-ms-client-request-id", _requestContext.CorrelationId.ToString()); | ||
request.BodyParameters.Add("client_id", certificateRequestResponse.ClientId); | ||
request.BodyParameters.Add("grant_type", certificateRequestResponse.Certificate); | ||
request.BodyParameters.Add("scope", "https://management.azure.com/.default"); | ||
request.RequestType = RequestType.Imds; | ||
request.MtlsCertificate = mtlsCertificate; | ||
|
||
return request; | ||
} | ||
|
||
/// <summary> | ||
/// Attaches a private key to a certificate for use in mTLS authentication. | ||
/// </summary> | ||
/// <param name="certificatePem">The certificate in PEM format</param> | ||
/// <param name="privateKey">The RSA private key to attach</param> | ||
/// <returns>An X509Certificate2 with the private key attached</returns> | ||
/// <exception cref="ArgumentNullException">Thrown when certificatePem or privateKey is null</exception> | ||
/// <exception cref="ArgumentException">Thrown when certificatePem is not a valid PEM certificate</exception> | ||
/// <exception cref="FormatException">Thrown when the certificate cannot be parsed</exception> | ||
internal X509Certificate2 AttachPrivateKeyToCert(string certificatePem, RSA privateKey) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could move all this X509 related code to the existing CommonCryptographyManager |
||
{ | ||
if (string.IsNullOrEmpty(certificatePem)) | ||
throw new ArgumentNullException(nameof(certificatePem)); | ||
if (privateKey == null) | ||
throw new ArgumentNullException(nameof(privateKey)); | ||
|
||
X509Certificate2 certificate; | ||
|
||
#if NET8_0_OR_GREATER | ||
// .NET 8.0+ has direct PEM parsing support | ||
certificate = X509Certificate2.CreateFromPem(certificatePem); | ||
// Attach the private key and return a new certificate instance | ||
return certificate.CopyWithPrivateKey(privateKey); | ||
#else | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// .NET Framework 4.7.2 and .NET Standard 2.0 - manual PEM parsing and private key attachment | ||
certificate = ParseCertificateFromPem(certificatePem); | ||
return AttachPrivateKeyToOlderFrameworks(certificate, privateKey); | ||
#endif | ||
} | ||
|
||
#if !NET8_0_OR_GREATER | ||
/// <summary> | ||
/// Parses a certificate from PEM format for older .NET versions. | ||
/// </summary> | ||
/// <param name="certificatePem">The certificate in PEM format</param> | ||
/// <returns>An X509Certificate2 instance</returns> | ||
/// <exception cref="ArgumentException">Thrown when the PEM format is invalid</exception> | ||
/// <exception cref="FormatException">Thrown when the Base64 content cannot be decoded</exception> | ||
internal static X509Certificate2 ParseCertificateFromPem(string certificatePem) | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
const string CertBeginMarker = "-----BEGIN CERTIFICATE-----"; | ||
const string CertEndMarker = "-----END CERTIFICATE-----"; | ||
|
||
int startIndex = certificatePem.IndexOf(CertBeginMarker, StringComparison.Ordinal); | ||
if (startIndex == -1) | ||
{ | ||
throw new ArgumentException("Invalid PEM format: missing BEGIN CERTIFICATE marker", nameof(certificatePem)); | ||
} | ||
|
||
startIndex += CertBeginMarker.Length; | ||
int endIndex = certificatePem.IndexOf(CertEndMarker, startIndex, StringComparison.Ordinal); | ||
if (endIndex == -1) | ||
{ | ||
throw new ArgumentException("Invalid PEM format: missing END CERTIFICATE marker", nameof(certificatePem)); | ||
} | ||
|
||
string base64Content = certificatePem.Substring(startIndex, endIndex - startIndex) | ||
.Replace("\r", "") | ||
.Replace("\n", "") | ||
.Replace(" ", ""); | ||
|
||
throw new NotImplementedException(); | ||
if (string.IsNullOrEmpty(base64Content)) | ||
{ | ||
throw new ArgumentException("Invalid PEM format: no certificate content found", nameof(certificatePem)); | ||
} | ||
|
||
try | ||
{ | ||
byte[] certBytes = Convert.FromBase64String(base64Content); | ||
return new X509Certificate2(certBytes); | ||
} | ||
catch (FormatException ex) | ||
{ | ||
throw new FormatException("Invalid PEM format: certificate content is not valid Base64", ex); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Attaches a private key to a certificate for older .NET Framework versions. | ||
/// This method uses the older RSACng approach for .NET Framework 4.7.2 and .NET Standard 2.0. | ||
/// </summary> | ||
/// <param name="certificate">The certificate without private key</param> | ||
/// <param name="privateKey">The RSA private key to attach</param> | ||
/// <returns>An X509Certificate2 with the private key attached</returns> | ||
/// <exception cref="NotSupportedException">Thrown when private key attachment fails</exception> | ||
internal X509Certificate2 AttachPrivateKeyToOlderFrameworks(X509Certificate2 certificate, RSA privateKey) | ||
{ | ||
try | ||
{ | ||
// For older frameworks, we need to use the legacy approach with RSACryptoServiceProvider | ||
// First, export the RSA parameters from the provided private key | ||
var parameters = privateKey.ExportParameters(includePrivateParameters: true); | ||
|
||
// Create a new RSACryptoServiceProvider with the correct key size | ||
int keySize = parameters.Modulus.Length * 8; | ||
var rsaProvider = new RSACryptoServiceProvider(keySize); | ||
|
||
try | ||
{ | ||
// Import the parameters into the new provider | ||
rsaProvider.ImportParameters(parameters); | ||
|
||
// Create a new certificate instance from the raw data | ||
var certWithPrivateKey = new X509Certificate2(certificate.RawData); | ||
|
||
// Assign the private key using the legacy property | ||
certWithPrivateKey.PrivateKey = rsaProvider; | ||
|
||
return certWithPrivateKey; | ||
} | ||
catch | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
// Clean up the RSA provider if something goes wrong | ||
rsaProvider?.Dispose(); | ||
throw; | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw new NotSupportedException( | ||
Robbie-Microsoft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"Failed to attach private key to certificate on this .NET Framework version.", ex); | ||
} | ||
} | ||
#endif | ||
|
||
private static string ImdsV2QueryParamsHelper(RequestContext requestContext) | ||
{ | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.