Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5498968
Initial commit. 2 TODOs
Robbie-Microsoft Aug 6, 2025
e04e408
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_csr
Robbie-Microsoft Aug 6, 2025
4e096b7
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_csr
Robbie-Microsoft Aug 6, 2025
6bc2164
Implemented CSR generator
Robbie-Microsoft Aug 6, 2025
762ccdf
first pass at improved unit tests
Robbie-Microsoft Aug 6, 2025
4ea6c09
Finished improving unit tests
Robbie-Microsoft Aug 6, 2025
009f948
Updates to CUID
Robbie-Microsoft Aug 7, 2025
21d4ef3
Unit test improvements
Robbie-Microsoft Aug 7, 2025
cd013a3
Implemented Feedback
Robbie-Microsoft Aug 7, 2025
480ae9e
renamed file
Robbie-Microsoft Aug 7, 2025
0aa8692
small improvement
Robbie-Microsoft Aug 8, 2025
de24670
Initial implementation
Robbie-Microsoft Aug 8, 2025
621c566
added missing awaitor for async method
Robbie-Microsoft Aug 8, 2025
07b7883
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 8, 2025
068461b
Fixed bugs discovered from unit testing in child branch
Robbie-Microsoft Aug 8, 2025
8a8439a
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 8, 2025
2034b25
undid changes to .proj
Robbie-Microsoft Aug 8, 2025
b415f99
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 8, 2025
2b7486a
undid change to global.json
Robbie-Microsoft Aug 8, 2025
a76d2fd
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 8, 2025
310c467
started unit testing
Robbie-Microsoft Aug 8, 2025
189ff9e
added missing sets
Robbie-Microsoft Aug 8, 2025
a98a5a8
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 8, 2025
9345e99
merged from parent branch
Robbie-Microsoft Aug 8, 2025
c72e61b
undid changes to global.json
Robbie-Microsoft Aug 8, 2025
92b325f
Inplemented some feedback
Robbie-Microsoft Aug 11, 2025
5ae0596
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 11, 2025
e85fc9a
merged from parent
Robbie-Microsoft Aug 11, 2025
067c83c
Implemented some feedback
Robbie-Microsoft Aug 14, 2025
f7d6f88
PKCS1 -> Pss padding
Robbie-Microsoft Aug 15, 2025
74e8e60
re-used imports
Robbie-Microsoft Aug 15, 2025
152f396
Implemented feedback
Robbie-Microsoft Aug 15, 2025
d46c853
Changes from manual testing.
Robbie-Microsoft Aug 19, 2025
3f75e3a
ImdsV2: Reworked Custom ASN1 Encoder to use System.Formats.Asn1 Nuget…
Robbie-Microsoft Aug 22, 2025
253993d
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_csr
Robbie-Microsoft Aug 22, 2025
c035de0
Merge branch 'rginsburg/msiv2_csr' into rginsburg/msiv2_acquire_entra…
Robbie-Microsoft Aug 22, 2025
755cf6f
Implemented some feedback
Robbie-Microsoft Aug 22, 2025
14d05f1
additional improvements
Robbie-Microsoft Aug 22, 2025
9b5e498
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_ac…
Robbie-Microsoft Aug 27, 2025
69e714a
fixed bad rebase
Robbie-Microsoft Aug 27, 2025
4219044
Fixed bad rebase
Robbie-Microsoft Aug 27, 2025
8d3c7ac
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_ac…
Robbie-Microsoft Aug 27, 2025
3bab6e4
Fixed bad rebase
Robbie-Microsoft Aug 27, 2025
6dacdf5
Adjusted variable names after rebase
Robbie-Microsoft Aug 27, 2025
0df6e1e
wrote the skeleton for the mTLS cert creation
Robbie-Microsoft Aug 27, 2025
a66a933
adjusted unit test based on new code
Robbie-Microsoft Aug 27, 2025
67cc4a6
Implemented mTLS
Robbie-Microsoft Aug 28, 2025
ca6f1d6
Removed un-used imports
Robbie-Microsoft Aug 28, 2025
3fc3ece
Undo changes to global.json
Robbie-Microsoft Aug 28, 2025
131b8cd
Implemented unit test + helpers
Robbie-Microsoft Aug 28, 2025
d839578
Undid changes to csproj
Robbie-Microsoft Aug 28, 2025
7118235
Implemented feedback
Robbie-Microsoft Aug 29, 2025
8fcac99
Improved unit tests. Added UAMI unit tests.
Robbie-Microsoft Aug 29, 2025
59aefb3
Merge branch 'rginsburg/msiv2_feature_branch' into rginsburg/msiv2_ac…
Robbie-Microsoft Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Microsoft.Identity.Client.Internal.Broker;
using Microsoft.Identity.Client.Internal.ClientCredential;
using Microsoft.Identity.Client.Kerberos;
using Microsoft.Identity.Client.ManagedIdentity.V2;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
using Microsoft.Identity.Client.UI;
using Microsoft.IdentityModel.Abstractions;
Expand Down Expand Up @@ -126,6 +127,7 @@ public string ClientVersion
public Func<AppTokenProviderParameters, Task<AppTokenProviderResult>> AppTokenProvider;

internal IRetryPolicyFactory RetryPolicyFactory { get; set; }
internal ICsrFactory CsrFactory { get; set; }

#region ClientCredentials

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.IdentityModel.Abstractions;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.Http.Retry;
using Microsoft.Identity.Client.ManagedIdentity.V2;

#if SUPPORTS_SYSTEM_TEXT_JSON
using System.Text.Json;
Expand All @@ -39,6 +40,12 @@ internal BaseAbstractApplicationBuilder(ApplicationConfiguration configuration)
{
Config.RetryPolicyFactory = new RetryPolicyFactory();
}

// Ensure the default csr factory is set if the test factory was not provided
if (Config.CsrFactory == null)
{
Config.CsrFactory = new DefaultCsrFactory();
}
}

internal ApplicationConfiguration Config { get; }
Expand Down Expand Up @@ -246,6 +253,17 @@ internal T WithRetryPolicyFactory(IRetryPolicyFactory factory)
return (T)this;
}

/// <summary>
/// Internal only: Allows tests to inject a custom csr factory.
/// </summary>
/// <param name="factory">The csr factory to use.</param>
/// <returns>The builder for chaining.</returns>
internal T WithCsrFactory(ICsrFactory factory)
{
Config.CsrFactory = factory;
return (T)this;
}

internal virtual ApplicationConfiguration BuildConfiguration()
{
ResolveAuthority();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(
method: HttpMethod.Get,
logger: _requestContext.Logger,
doNotThrow: true,
mtlsCertificate: null,
mtlsCertificate: request.MtlsCertificate,
validateServerCertificate: GetValidationCallback(),
cancellationToken: cancellationToken,
retryPolicy: retryPolicy).ConfigureAwait(false);
Expand All @@ -97,7 +97,7 @@ public virtual async Task<ManagedIdentityResponse> AuthenticateAsync(
method: HttpMethod.Post,
logger: _requestContext.Logger,
doNotThrow: true,
mtlsCertificate: null,
mtlsCertificate: request.MtlsCertificate,
validateServerCertificate: GetValidationCallback(),
cancellationToken: cancellationToken,
retryPolicy: retryPolicy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.ApiConfig.Parameters;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.OAuth2;
Expand All @@ -26,14 +27,21 @@ internal class ManagedIdentityRequest

public RequestType RequestType { get; set; }

public ManagedIdentityRequest(HttpMethod method, Uri endpoint, RequestType requestType = RequestType.ManagedIdentityDefault)
public X509Certificate2 MtlsCertificate { get; set; }

public ManagedIdentityRequest(
HttpMethod method,
Uri endpoint,
RequestType requestType = RequestType.ManagedIdentityDefault,
X509Certificate2 mtlsCertificate = null)
{
Method = method;
_baseEndpoint = endpoint;
Headers = new Dictionary<string, string>();
BodyParameters = new Dictionary<string, string>();
QueryParameters = new Dictionary<string, string>();
RequestType = requestType;
MtlsCertificate = mtlsCertificate;
}

public Uri ComputeUri()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
internal class Csr
{
internal static string Generate(string clientId, string tenantId, CuidInfo cuid)
internal static (string csrPem, RSA privateKey) Generate(string clientId, string tenantId, CuidInfo cuid)
{
using (RSA rsa = CreateRsaKeyPair())
{
Expand All @@ -30,7 +30,7 @@ internal static string Generate(string clientId, string tenantId, CuidInfo cuid)
"1.3.6.1.4.1.311.90.2.10",
writer.Encode()));

return req.CreateSigningRequestPem();
return (req.CreateSigningRequestPem(), rsa);
}
}

Expand Down
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);
}
}
}
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Identity.Client.Http;
using Microsoft.Identity.Client.Http.Retry;
using Microsoft.Identity.Client.Internal;
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client.ManagedIdentity.V2
Expand All @@ -20,11 +21,16 @@ 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,
bool probeMode)
{
#if NET462
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.");
return await Task.FromResult<CsrMetadata>(null).ConfigureAwait(false);
#else
var queryParams = ImdsV2QueryParamsHelper(requestContext);

var headers = new Dictionary<string, string>
Expand Down Expand Up @@ -90,6 +96,7 @@ public static async Task<CsrMetadata> GetCsrMetadataAsync(
}

return TryCreateCsrMetadata(response, requestContext.Logger, probeMode);
#endif
}

private static void ThrowProbeFailedException(
Expand Down Expand Up @@ -251,11 +258,24 @@ 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);

throw new NotImplementedException();

// transform certificateRequestResponse.Certificate to x509 with private key
var mtlsCertificate = CommonCryptographyManager.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;
}

private static string ImdsV2QueryParamsHelper(RequestContext requestContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,114 @@ byte[] SignDataAndCacheProvider(string message)
return signedData;
}
}

/// <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 static X509Certificate2 AttachPrivateKeyToCert(string certificatePem, RSA privateKey)
{
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
// .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>
private static X509Certificate2 ParseCertificateFromPem(string certificatePem)
{
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(" ", "");

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>
private static X509Certificate2 AttachPrivateKeyToOlderFrameworks(X509Certificate2 certificate, RSA privateKey)
{
// 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;
using (var rsaProvider = new RSACryptoServiceProvider(keySize))
{
// 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;
}
}
#endif
}
}
Loading