diff --git a/src/client/Microsoft.Identity.Client/Internal/JsonWebToken.cs b/src/client/Microsoft.Identity.Client/Internal/JsonWebToken.cs index 34a7322854..b060cd239c 100644 --- a/src/client/Microsoft.Identity.Client/Internal/JsonWebToken.cs +++ b/src/client/Microsoft.Identity.Client/Internal/JsonWebToken.cs @@ -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); + } + } + + private string SignToken( + string token, + X509Certificate2 certificate, + RSASignaturePadding signaturePadding) + { if (MaxTokenLength < token.Length) { throw new MsalClientException(MsalError.EncodedTokenTooLong); @@ -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)); } @@ -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); } } diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs index 3a0761a41d..71d0b1b5d2 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ClientCredentialWithCertTest.cs @@ -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; using System.Threading.Tasks; @@ -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 { @@ -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 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); + } + } + +#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); + } + } + private static void AssertClientAssertionHeader( X509Certificate2 cert, JwtSecurityToken decodedToken,