Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 41 additions & 10 deletions src/client/Microsoft.Identity.Client/Internal/JsonWebToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,46 @@ private string CreateJsonPayload()

public string Sign(X509Certificate2 certificate, bool sendX5C, bool useSha2AndPss)
{
string encodedPayload = Base64UrlHelpers.EncodeString(CreateJsonPayload());

// Base64Url encoded header and claims
string token = CreateJwtHeaderAndBody(certificate, sendX5C, useSha2AndPss);
string token = CreateJwtHeaderAndBody(
certificate,
sendX5C,
useSha2AndPss,
encodedPayload);

// Length check before sign
try
{
return SignToken(
token,
certificate,
useSha2AndPss ?
RSASignaturePadding.Pss : // ESTS added support for PSS
RSASignaturePadding.Pkcs1); // Other IdPs may only support PKCS1
}
catch (CryptographicException) when (useSha2AndPss)
{
// Some private key providers do not support PSS. Retry with an RS256 header
// so the JWT algorithm and thumbprint remain consistent with PKCS#1 signing.
string fallbackToken = CreateJwtHeaderAndBody(
certificate,
sendX5C,
useSha2AndPss: false,
encodedPayload);

return SignToken(
fallbackToken,
certificate,
RSASignaturePadding.Pkcs1);
}
Comment thread
bgavrilMS marked this conversation as resolved.
}

private string SignToken(
string token,
X509Certificate2 certificate,
RSASignaturePadding signaturePadding)
{
if (MaxTokenLength < token.Length)
{
throw new MsalClientException(MsalError.EncodedTokenTooLong);
Expand All @@ -140,9 +176,7 @@ public string Sign(X509Certificate2 certificate, bool sendX5C, bool useSha2AndPs
byte[] signature = _cryptographyManager.SignWithCertificate(
token,
certificate,
useSha2AndPss ?
RSASignaturePadding.Pss : // ESTS added support for PSS
RSASignaturePadding.Pkcs1); // Other IdPs may only support PKCS1
signaturePadding);

return string.Concat(token, ".", Base64UrlHelpers.Encode(signature));
}
Expand Down Expand Up @@ -206,15 +240,12 @@ private static string ComputeCertThumbprint(X509Certificate2 certificate, bool u
private string CreateJwtHeaderAndBody(
X509Certificate2 certificate,
bool addX5C,
bool useSha2AndPss)
bool useSha2AndPss,
string encodedPayload)
{

string jsonHeader = CreateJsonHeader(certificate, addX5C, useSha2AndPss);
string encodedHeader = Base64UrlHelpers.EncodeString(jsonHeader);

string jsonPayload = CreateJsonPayload();
string encodedPayload = Base64UrlHelpers.EncodeString(jsonPayload);

return string.Concat(encodedHeader, ".", encodedPayload);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading;
Comment thread
bgavrilMS marked this conversation as resolved.
using System.Threading.Tasks;
Expand All @@ -24,6 +25,13 @@
using Microsoft.Identity.Test.Common.Core.Mocks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Identity.Client.Extensibility;
using Microsoft.Identity.Client.Internal.Logger;
using Microsoft.Identity.Client.PlatformsCommon.Interfaces;
#if NETFRAMEWORK
using Microsoft.Identity.Client.Platforms.netdesktop;
#else
using Microsoft.Identity.Client.Platforms.netstandard;
#endif

namespace Microsoft.Identity.Test.Unit
{
Expand Down Expand Up @@ -658,6 +666,127 @@ public void ClientAssertionTests(bool sendX5C, bool useSha2AndPss, bool addExtra
}
}

[TestMethod]
public async Task AcquireTokenForClientWhenPssSigningFailsSendsRs256AssertionAsync()
{
// Arrange
using (var harness = CreateTestHarness())
using (RSA rsa = RSA.Create(2048))
using (X509Certificate2 certificate = new CertificateRequest(
"CN=PSS Fallback Test",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1).CreateSelfSigned(
DateTimeOffset.UtcNow.AddMinutes(-1),
DateTimeOffset.UtcNow.AddDays(1)))
{
SetupMocks(harness.HttpManager);
var cryptographyManager = new PssFailingCryptographyManager();

var app = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithAuthority(new Uri(ClientApplicationBase.DefaultAuthority), true)
.WithHttpManager(harness.HttpManager)
.WithPlatformProxy(new PssFailingPlatformProxy(cryptographyManager))
.WithCertificate(certificate)
.BuildConcrete();

MockHttpMessageHandler handler = CreateTokenResponseHttpHandler(clientCredentialFlow: true);
handler.AdditionalRequestValidation = request =>
{
string requestContent = request.Content.ReadAsStringAsync().GetAwaiter().GetResult();
IDictionary<string, string> formData = CoreHelpers.ParseKeyValueList(
requestContent,
'&',
true,
null);

Assert.IsTrue(
formData.TryGetValue(OAuth2Parameter.ClientAssertion, out string encodedAssertion),
"Missing client_assertion from request.");

var assertion = new JwtSecurityTokenHandler().ReadJwtToken(encodedAssertion);
AssertClientAssertionHeader(
certificate,
assertion,
sendX5c: true,
useSha2AndPss: false);

string[] assertionSegments = encodedAssertion.Split('.');
Assert.HasCount(3, assertionSegments);

byte[] signingInput = Encoding.UTF8.GetBytes(
assertionSegments[0] + "." + assertionSegments[1]);
byte[] signature = Base64UrlHelpers.DecodeBytes(assertionSegments[2]);

using (RSA publicKey = certificate.GetRSAPublicKey())
{
Assert.IsTrue(
publicKey.VerifyData(
signingInput,
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1),
"The client_assertion signature should be valid RS256.");
}
};

harness.HttpManager.AddMockHandler(handler);

// Act
AuthenticationResult result = await app
.AcquireTokenForClient(TestConstants.s_scope)
.WithSendX5C(true)
.ExecuteAsync(CancellationToken.None)
.ConfigureAwait(false);

// Assert
Assert.IsNotNull(result.AccessToken);
Assert.AreEqual(1, cryptographyManager.PssSigningAttempts);
Assert.AreEqual(1, cryptographyManager.Pkcs1SigningAttempts);
}
Comment thread
bgavrilMS marked this conversation as resolved.
}

#if NETFRAMEWORK
private sealed class PssFailingPlatformProxy : NetDesktopPlatformProxy
#else
private sealed class PssFailingPlatformProxy : NetCorePlatformProxy
#endif
{
private readonly ICryptographyManager _cryptographyManager;

public PssFailingPlatformProxy(ICryptographyManager cryptographyManager)
: base(new NullLogger())
{
_cryptographyManager = cryptographyManager;
}

protected override ICryptographyManager InternalGetCryptographyManager()
=> _cryptographyManager;
}

private sealed class PssFailingCryptographyManager : CommonCryptographyManager
{
public int PssSigningAttempts { get; private set; }

public int Pkcs1SigningAttempts { get; private set; }

public override byte[] SignWithCertificate(
string message,
X509Certificate2 certificate,
RSASignaturePadding signaturePadding)
{
if (signaturePadding == RSASignaturePadding.Pss)
{
PssSigningAttempts++;
throw new CryptographicException("PSS signing is not supported by this key provider.");
}

Pkcs1SigningAttempts++;
return base.SignWithCertificate(message, certificate, signaturePadding);
}
}
Comment thread
bgavrilMS marked this conversation as resolved.

private static void AssertClientAssertionHeader(
X509Certificate2 cert,
JwtSecurityToken decodedToken,
Expand Down
Loading