Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 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
621c566
added missing awaitor for async method
Robbie-Microsoft Aug 8, 2025
068461b
Fixed bugs discovered from unit testing in child branch
Robbie-Microsoft Aug 8, 2025
2034b25
undid changes to .proj
Robbie-Microsoft Aug 8, 2025
2b7486a
undid change to global.json
Robbie-Microsoft Aug 8, 2025
189ff9e
added missing sets
Robbie-Microsoft Aug 8, 2025
92b325f
Inplemented some feedback
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
3481c39
Implemented feedback
Robbie-Microsoft Aug 25, 2025
92158bb
Small rework due to spec changes
Robbie-Microsoft Aug 25, 2025
729a56a
Additional rework due to spec changes
Robbie-Microsoft Aug 25, 2025
3027392
Implemented feedback
Robbie-Microsoft Aug 25, 2025
3c3dcdf
Removed null check on vmId. Created CuidInfo.IsNullOrEmpty
Robbie-Microsoft Aug 25, 2025
f51cdf9
Implemented feedback
Robbie-Microsoft Aug 26, 2025
5e7ab07
Updated min version of imds, spec has incorrect info
Robbie-Microsoft Aug 26, 2025
362b407
Updated a comment
Robbie-Microsoft Aug 27, 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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<PackageVersion Include="System.ComponentModel.TypeConverter" Version="4.3.0" />
<!-- Should match Azure Functions runtime: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/4456 -->
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="6.0.1" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.8" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Serialization.Formatters" Version="4.3.0" />
Expand Down Expand Up @@ -80,6 +81,5 @@
<PackageVersion Include="System.ValueTuple" Version="4.5.0" />
<PackageVersion Include="System.Windows.Forms" Version="4.0.0" />
<PackageVersion Include="CommandLineParser" Version="2.8.0" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Identity.Client.PlatformsCommon.Shared;
using System.IO;
using Microsoft.Identity.Client.Core;
using Microsoft.Identity.Client.ManagedIdentity.V2;

namespace Microsoft.Identity.Client.ManagedIdentity
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.ObjectModel;
using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
internal class CertificateRequest
{
private X500DistinguishedName _subjectName;
private RSA _rsa;
private HashAlgorithmName _hashAlgorithmName;
private RSASignaturePadding _rsaPadding;

internal CertificateRequest(
X500DistinguishedName subjectName,
RSA key,
HashAlgorithmName hashAlgorithm,
RSASignaturePadding padding)
{
_subjectName = subjectName;
_rsa = key;
_hashAlgorithmName = hashAlgorithm;
_rsaPadding = padding;
}

internal Collection<AsnEncodedData> OtherRequestAttributes { get; } = new Collection<AsnEncodedData>();

private static string MakePem(byte[] ber, string header)
{
const int LineLength = 64;

string base64 = Convert.ToBase64String(ber);
int offset = 0;

StringBuilder builder = new StringBuilder("-----BEGIN ");
builder.Append(header);
builder.AppendLine("-----");

while (offset < base64.Length)
{
int lineEnd = Math.Min(offset + LineLength, base64.Length);
builder.AppendLine(base64.Substring(offset, lineEnd - offset));
offset = lineEnd;
}

builder.Append("-----END ");
builder.Append(header);
builder.AppendLine("-----");

return builder.ToString();
}

internal string CreateSigningRequestPem()
{
byte[] csr = CreateSigningRequest();
return MakePem(csr, "CERTIFICATE REQUEST");
}

internal byte[] CreateSigningRequest()
{
if (_hashAlgorithmName != HashAlgorithmName.SHA256)
{
throw new NotSupportedException("Signature Processing has only been written for SHA256");
}

AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);

// RSAPublicKey ::= SEQUENCE {
// modulus INTEGER, -- n
// publicExponent INTEGER -- e
// }

using (writer.PushSequence())
{
RSAParameters rsaParameters = _rsa.ExportParameters(false);
writer.WriteIntegerUnsigned(rsaParameters.Modulus);
writer.WriteIntegerUnsigned(rsaParameters.Exponent);
}

byte[] publicKey = writer.Encode();
writer.Reset();

// CertificationRequestInfo ::= SEQUENCE {
// version INTEGER { v1(0) } (v1,...),
// subject Name,
// subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }},
// attributes [0] Attributes{{ CRIAttributes }}
// }
//
// SubjectPublicKeyInfo { ALGORITHM: IOSet} ::= SEQUENCE {
// algorithm AlgorithmIdentifier { { IOSet} },
// subjectPublicKey BIT STRING
// }
//
// Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }}
//
// Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE {
// type ATTRIBUTE.&id({IOSet}),
// values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type})
// }

using (writer.PushSequence())
{
writer.WriteInteger(0);
writer.WriteEncodedValue(_subjectName.RawData);

// subjectPKInfo
using (writer.PushSequence())
{
// algorithm
using (writer.PushSequence())
{
writer.WriteObjectIdentifier("1.2.840.113549.1.1.1");
// RSA uses an explicit NULL value for parameters
writer.WriteNull();
}

writer.WriteBitString(publicKey);
}

if (OtherRequestAttributes.Count > 0)
{
// attributes
using (writer.PushSetOf(new Asn1Tag(TagClass.ContextSpecific, 0)))
{
foreach (AsnEncodedData attribute in OtherRequestAttributes)
{
using (writer.PushSequence())
{
writer.WriteObjectIdentifier(attribute.Oid.Value);

using (writer.PushSetOf())
{
writer.WriteEncodedValue(attribute.RawData);
}
}
}
}
}
}

byte[] certReqInfo = writer.Encode();
writer.Reset();

// CertificationRequest ::= SEQUENCE {
// certificationRequestInfo CertificationRequestInfo,
// signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }},
// signature BIT STRING
// }

using (writer.PushSequence())
{
writer.WriteEncodedValue(certReqInfo);

// signatureAlgorithm
using (writer.PushSequence())
{
if (_rsaPadding == RSASignaturePadding.Pss)
{
if (_hashAlgorithmName != HashAlgorithmName.SHA256)
{
throw new NotSupportedException("Only SHA256 is supported with PSS padding.");
}

writer.WriteObjectIdentifier("1.2.840.113549.1.1.10");

// RSASSA-PSS-params ::= SEQUENCE {
// hashAlgorithm [0] HashAlgorithm DEFAULT sha1,
// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1,
// saltLength [2] INTEGER DEFAULT 20,
// trailerField [3] TrailerField DEFAULT trailerFieldBC
// }

using (writer.PushSequence())
{
string digestOid = "2.16.840.1.101.3.4.2.1";

// hashAlgorithm
using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0)))
{
using (writer.PushSequence())
{
writer.WriteObjectIdentifier(digestOid);
}
}

using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 1)))
{
using (writer.PushSequence())
{
writer.WriteObjectIdentifier("1.2.840.113549.1.1.8");

using (writer.PushSequence())
{
writer.WriteObjectIdentifier(digestOid);
}
}
}

// saltLength (SHA256.Length, 32 bytes)
using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 2)))
{
writer.WriteInteger(32);
}

// trailerField 1, which is trailerFieldBC, which is the DEFAULT,
// so don't write it down.
}
}
else if (_rsaPadding == RSASignaturePadding.Pkcs1)
{
writer.WriteObjectIdentifier("1.2.840.113549.1.1.11");
// RSA PKCS1 uses an explicit NULL value for parameters
writer.WriteNull();
}
else
{
throw new NotSupportedException("Unsupported RSA padding.");
}
}

byte[] signature = _rsa.SignData(certReqInfo, _hashAlgorithmName, _rsaPadding);
writer.WriteBitString(signature);
}

return writer.Encode();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Net;
#if SUPPORTS_SYSTEM_TEXT_JSON
using JsonProperty = System.Text.Json.Serialization.JsonPropertyNameAttribute;
#else
using Microsoft.Identity.Json;
#endif

namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
/// <summary>
/// Represents the response for a Managed Identity CSR request.
/// </summary>
internal class CertificateRequestResponse
{
[JsonProperty("client_id")]
public string ClientId { get; set; }

[JsonProperty("tenant_id")]
public string TenantId { get; set; }

[JsonProperty("client_credential")]
public string ClientCredential { get; set; }

[JsonProperty("regional_token_url")]
public string RegionalTokenUrl { get; set; }

[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }

[JsonProperty("refresh_in")]
public int RefreshIn { get; set; }

public CertificateRequestResponse() { }

public static void Validate(CertificateRequestResponse certificateRequestResponse)
{
if (string.IsNullOrEmpty(certificateRequestResponse.ClientId) ||
string.IsNullOrEmpty(certificateRequestResponse.TenantId) ||
string.IsNullOrEmpty(certificateRequestResponse.ClientCredential) ||
string.IsNullOrEmpty(certificateRequestResponse.RegionalTokenUrl) ||
certificateRequestResponse.ExpiresIn <= 0 ||
certificateRequestResponse.RefreshIn <= 0)
{
throw MsalServiceExceptionFactory.CreateManagedIdentityException(
MsalError.ManagedIdentityRequestFailed,
$"[ImdsV2] ImdsV2ManagedIdentitySource.ExecuteCertificateRequestAsync failed because the certificate request response is malformed. Status code: 200",
null,
ManagedIdentitySource.ImdsV2,
(int)HttpStatusCode.OK);
}
}
}
}
51 changes: 51 additions & 0 deletions src/client/Microsoft.Identity.Client/ManagedIdentity/V2/Csr.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Identity.Client.Utils;

namespace Microsoft.Identity.Client.ManagedIdentity.V2
{
internal class Csr
{
internal static string Generate(string clientId, string tenantId, CuidInfo cuid)
{
using (RSA rsa = CreateRsaKeyPair())
{
CertificateRequest req = new CertificateRequest(
new X500DistinguishedName($"CN={clientId}, DC={tenantId}"),
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pss);

AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
writer.WriteCharacterString(UniversalTagNumber.UTF8String, JsonHelper.SerializeToJson(cuid));

req.OtherRequestAttributes.Add(
new AsnEncodedData(
"1.2.840.113549.1.9.7",
writer.Encode()));

return req.CreateSigningRequestPem();
}
}

private static RSA CreateRsaKeyPair()
{
// TODO: use the strongest key on the machine i.e. a TPM key
RSA rsa = null;

#if NET462 || NET472
// .NET Framework runs only on Windows, so RSACng (Windows-only) is always available
rsa = new RSACng();
#else
// Cross-platform .NET - RSA.Create() returns appropriate PSS-capable implementation
rsa = RSA.Create();
#endif
rsa.KeySize = 2048;
return rsa;
}
}
}
Loading