diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/Experimental/JsonWebTokenHandler.DecryptToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/Experimental/JsonWebTokenHandler.DecryptToken.cs index 5242b7472f..4862560994 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/Experimental/JsonWebTokenHandler.DecryptToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/Experimental/JsonWebTokenHandler.DecryptToken.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; +using System.Security.Cryptography; +using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens.Experimental; @@ -137,9 +138,15 @@ internal ValidationResult DecryptToken( return (keys, null); // Cannot iterate over null. var unwrappedKeys = new List(); - // keep track of exceptions thrown, keys that were tried - StringBuilder? exceptionStrings = null; - StringBuilder? keysAttempted = null; + + // Pre-generate a placeholder key used as a fallback when an unwrap call + // does not yield a usable key, so that downstream processing follows a + // single uniform path regardless of which key was tried. + int expectedCekSizeInBytes = GetExpectedCekSizeInBytes(jwtToken.Enc); + byte[] fallbackCek = new byte[expectedCekSizeInBytes]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + rng.GetBytes(fallbackCek); + for (int i = 0; i < keys.Count; i++) { var key = keys[i]; @@ -182,27 +189,16 @@ internal ValidationResult DecryptToken( catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { - (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); - } + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(ex.ToString()); - (keysAttempted ??= new StringBuilder()).AppendLine(key.KeyId); + // Use the placeholder key so the downstream code path is the + // same regardless of whether unwrap succeeded. + unwrappedKeys.Add(new SymmetricSecurityKey(fallbackCek)); + } } - if (unwrappedKeys.Count > 0 || exceptionStrings is null) - return (unwrappedKeys, null); - else - { - ValidationError validationError = new( - new MessageDetail( - TokenLogMessages.IDX10618, - LogHelper.MarkAsNonPII(keysAttempted?.ToString() ?? ""), - exceptionStrings?.ToString() ?? "", - LogHelper.MarkAsSecurityArtifact(jwtToken, JwtTokenUtilities.SafeLogJwtToken)), - ValidationFailureType.KeyWrapFailed, - ValidationError.GetCurrentStackFrame()); - - return (null, validationError); - } + return (unwrappedKeys, null); } /// diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index e79d99b37a..d797ec1ae1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Security.Claims; +using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -1332,9 +1333,15 @@ internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken return keys; var unwrappedKeys = new List(); - // keep track of exceptions thrown, keys that were tried - StringBuilder exceptionStrings = null; - StringBuilder keysAttempted = null; + + // Pre-generate a placeholder key used as a fallback when an unwrap call + // does not yield a usable key, so that downstream processing follows a + // single uniform path regardless of which key was tried. + int expectedCekSizeInBytes = GetExpectedCekSizeInBytes(jwtToken.Enc); + byte[] fallbackCek = new byte[expectedCekSizeInBytes]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + rng.GetBytes(fallbackCek); + if (keys != null) { foreach (var key in keys) @@ -1387,22 +1394,43 @@ internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken } catch (Exception ex) { - (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); - } + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(ex.ToString()); - (keysAttempted ??= new StringBuilder()).AppendLine(key.KeyId); + // Use the placeholder key so the downstream code path is the + // same regardless of whether unwrap succeeded. + unwrappedKeys.Add(new SymmetricSecurityKey(fallbackCek)); + } } } - if (unwrappedKeys.Count > 0 || exceptionStrings is null) - return unwrappedKeys; - else - throw LogHelper.LogExceptionMessage( - new SecurityTokenKeyWrapException( - LogHelper.FormatInvariant( - TokenLogMessages.IDX10618, - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII((object)exceptionStrings ?? ""), - jwtToken))); + + return unwrappedKeys; + } + + /// + /// Returns the expected Content Encryption Key (CEK) size in bytes for + /// the given content encryption algorithm (JWE "enc" header value). + /// + private static int GetExpectedCekSizeInBytes(string encAlgorithm) + { + // CBC algorithms use a composite key (AES + HMAC), so CEK is double the AES key size. + if (SecurityAlgorithms.Aes128CbcHmacSha256.Equals(encAlgorithm, StringComparison.Ordinal)) + return 32; + if (SecurityAlgorithms.Aes192CbcHmacSha384.Equals(encAlgorithm, StringComparison.Ordinal)) + return 48; + if (SecurityAlgorithms.Aes256CbcHmacSha512.Equals(encAlgorithm, StringComparison.Ordinal)) + return 64; + + // GCM algorithms use a single AES key. + if (SecurityAlgorithms.Aes128Gcm.Equals(encAlgorithm, StringComparison.Ordinal)) + return 16; + if (SecurityAlgorithms.Aes192Gcm.Equals(encAlgorithm, StringComparison.Ordinal)) + return 24; + if (SecurityAlgorithms.Aes256Gcm.Equals(encAlgorithm, StringComparison.Ordinal)) + return 32; + + // Default for unknown algorithms. + return 32; } } } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/BoundedXmlDictionaryReaderQuotas.cs b/src/Microsoft.IdentityModel.Tokens.Saml/BoundedXmlDictionaryReaderQuotas.cs new file mode 100644 index 0000000000..335e039b16 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens.Saml/BoundedXmlDictionaryReaderQuotas.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Xml; + +namespace Microsoft.IdentityModel.Tokens.Saml +{ + /// + /// Provides with a bounded + /// suitable for parsing XML received from external sources. + /// + internal static class BoundedXmlDictionaryReaderQuotas + { + /// + /// Bounded quotas with = 32. All other limits match . + /// + internal static XmlDictionaryReaderQuotas Quotas { get; } = new XmlDictionaryReaderQuotas + { + MaxDepth = 32, + MaxStringContentLength = int.MaxValue, + MaxArrayLength = int.MaxValue, + MaxBytesPerRead = int.MaxValue, + MaxNameTableCharCount = int.MaxValue + }; + } +} diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt index e69de29bb2..a8aa0b93eb 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens.Saml/InternalAPI.Unshipped.txt @@ -0,0 +1,5 @@ +const Microsoft.IdentityModel.Tokens.Saml.LogMessages.IDX11315 = "IDX11315: Unable to validate token. SamlSecurityToken.Assertion is null or empty." -> string +const Microsoft.IdentityModel.Tokens.Saml.LogMessages.IDX11138 = "IDX11138: SAML assertion nesting bound exceeded (depth: '{0}', bound: '{1}')." -> string +const Microsoft.IdentityModel.Tokens.Saml2.LogMessages.IDX13111 = "IDX13111: SAML2 assertion nesting bound exceeded (depth: '{0}', bound: '{1}')." -> string +Microsoft.IdentityModel.Tokens.Saml.BoundedXmlDictionaryReaderQuotas +static Microsoft.IdentityModel.Tokens.Saml.BoundedXmlDictionaryReaderQuotas.Quotas.get -> System.Xml.XmlDictionaryReaderQuotas diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Experimental/SamlSecurityTokenHandler.ReadToken.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Experimental/SamlSecurityTokenHandler.ReadToken.cs index 219c64d11f..a018b348f0 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Experimental/SamlSecurityTokenHandler.ReadToken.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/Experimental/SamlSecurityTokenHandler.ReadToken.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -36,7 +36,7 @@ internal virtual ValidationResult ReadSamlToken( try { - using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), XmlDictionaryReaderQuotas.Max)) + using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), BoundedXmlDictionaryReaderQuotas.Quotas)) { return ReadSamlToken(reader); } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs index b83a539bcb..f4ceaf28d9 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/LogMessages.cs @@ -56,6 +56,7 @@ internal static class LogMessages internal const string IDX11135 = "IDX11135: Unable to read SamlSecurityToken. Saml element '{0}' must have value."; internal const string IDX11136 = "IDX11136: 'AuthorizationDecisionStatement' cannot be empty."; internal const string IDX11137 = "IDX11137: 'SamlAction' must have a value."; + internal const string IDX11138 = "IDX11138: SAML assertion nesting bound exceeded (depth: '{0}', bound: '{1}')."; // Saml writting internal const string IDX11501 = "IDX11501: SamlAssertion Id cannot be null or empty."; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index e65e8bc09d..d17a75f507 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -758,7 +758,7 @@ public virtual SamlSecurityToken ReadSamlToken(string token) if (token.Length > MaximumTokenSizeInBytes) throw LogExceptionMessage(new ArgumentException(FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))); - using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), XmlDictionaryReaderQuotas.Max)) + using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), BoundedXmlDictionaryReaderQuotas.Quotas)) { return ReadSamlToken(reader); } @@ -853,7 +853,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI { if (attributeValue != null && attributeValue.Length > 0) { - using (var xmlReader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(attributeValue), XmlDictionaryReaderQuotas.Max)) + using (var xmlReader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(attributeValue), BoundedXmlDictionaryReaderQuotas.Quotas)) { xmlReader.MoveToContent(); xmlReader.ReadStartElement(Actor); diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSerializer.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSerializer.cs index bf0c47a6f4..934acaa748 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSerializer.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSerializer.cs @@ -15,6 +15,9 @@ namespace Microsoft.IdentityModel.Tokens.Saml /// public class SamlSerializer { + private const int MaxDepth = 8; + [ThreadStatic] + private static int t_currentDepth; private DSigSerializer _dsigSerializer = DSigSerializer.Default; private string _prefix = SamlConstants.Prefix; @@ -198,8 +201,15 @@ public virtual SamlAssertion ReadAssertion(XmlReader reader) { XmlUtil.CheckReaderOnEntry(reader, SamlConstants.Elements.Assertion, SamlConstants.Namespace); + t_currentDepth++; try { + if (t_currentDepth >= MaxDepth) + throw LogReadException( + LogMessages.IDX11138, + t_currentDepth, + MaxDepth); + var envelopeReader = new EnvelopedSignatureReader(reader) { Serializer = DSigSerializer }; // @xsi:type @@ -292,6 +302,10 @@ public virtual SamlAssertion ReadAssertion(XmlReader reader) throw LogReadException(LogMessages.IDX11122, ex, SamlConstants.Elements.Assertion, ex); } + finally + { + t_currentDepth--; + } } /// diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Experimental/Saml2SecurityTokenHandler.ReadToken.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Experimental/Saml2SecurityTokenHandler.ReadToken.cs index 64b06675f3..33298d32ba 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Experimental/Saml2SecurityTokenHandler.ReadToken.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Experimental/Saml2SecurityTokenHandler.ReadToken.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -34,7 +34,7 @@ internal virtual ValidationResult ReadSaml2Token try { - using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), XmlDictionaryReaderQuotas.Max)) + using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), Saml.BoundedXmlDictionaryReaderQuotas.Quotas)) { return ReadSaml2Token(reader); } diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs index d60b4b4a5a..0fd9f7516d 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/LogMessages.cs @@ -38,6 +38,7 @@ internal static class LogMessages internal const string IDX13108 = "IDX13108: When reading '{0}', Assertion.Subject is null and no Statements were found. [Saml2Core, line 585]."; internal const string IDX13109 = "IDX13109: When reading '{0}', Assertion.Subject is null and an Authentication, Attribute or AuthorizationDecision Statement was found. and no Statements were found. [Saml2Core, lines 1050, 1168, 1280]."; internal const string IDX13110 = "IDX13110: The Saml2SecurityToken must have a value for its Assertion property."; + internal const string IDX13111 = "IDX13111: SAML2 assertion nesting bound exceeded (depth: '{0}', bound: '{1}')."; internal const string IDX13117 = "IDX13117: A was encountered while processing the attribute statement.To handle encrypted attributes, extend the Saml2SecurityTokenHandler and override ReadAttributeStatement."; internal const string IDX13118 = "IDX13118: A element was encountered.To handle by-value authentication context declarations, extend Saml2SecurityTokenHandler and override ReadAuthenticationContext.In addition, it may be necessary to extend Saml2AuthenticationContext so that its data model can accommodate the declaration value."; internal const string IDX13119 = "IDX13119: An abstract element was encountered which does not specify its concrete type. Element name: '{0}' Namespace: '{1}'"; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs index 68b8a0703a..bff3e1cf3c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.cs @@ -578,7 +578,7 @@ public virtual Saml2SecurityToken ReadSaml2Token(string token) if (token.Length > MaximumTokenSizeInBytes) throw LogExceptionMessage(new ArgumentException(FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))); - using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), XmlDictionaryReaderQuotas.Max)) + using (var reader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(token), BoundedXmlDictionaryReaderQuotas.Quotas)) { return ReadSaml2Token(reader); } @@ -1117,7 +1117,7 @@ protected virtual void SetClaimsIdentityActorFromAttribute(Saml2Attribute attrib { if (value != null) { - using (var dictionaryReader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(value), XmlDictionaryReaderQuotas.Max)) + using (var dictionaryReader = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(value), BoundedXmlDictionaryReaderQuotas.Quotas)) { dictionaryReader.MoveToContent(); dictionaryReader.ReadStartElement(_actor); diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs index 87511d4f33..3983615d9c 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2Serializer.cs @@ -15,6 +15,9 @@ namespace Microsoft.IdentityModel.Tokens.Saml2 /// public class Saml2Serializer { + private const int MaxDepth = 8; + [ThreadStatic] + private static int t_currentDepth; private DSigSerializer _dsigSerializer = DSigSerializer.Default; private string _prefix = Saml2Constants.Prefix; @@ -185,8 +188,15 @@ public virtual Saml2Assertion ReadAssertion(XmlReader reader) var envelopeReader = new EnvelopedSignatureReader(reader) { Serializer = DSigSerializer }; var assertion = new Saml2Assertion(new Saml2NameIdentifier("__TemporaryIssuer__")); + t_currentDepth++; try { + if (t_currentDepth >= MaxDepth) + throw LogReadException( + LogMessages.IDX13111, + t_currentDepth, + MaxDepth); + // @xsi:type XmlUtil.ValidateXsiType(envelopeReader, Saml2Constants.Types.AssertionType, Saml2Constants.Namespace); @@ -282,6 +292,10 @@ public virtual Saml2Assertion ReadAssertion(XmlReader reader) throw LogReadException(LogMessages.IDX13102, ex, Saml2Constants.Elements.Assertion, ex); } + finally + { + t_currentDepth--; + } } /// @@ -956,6 +970,8 @@ protected virtual Saml2Evidence ReadEvidence(XmlDictionaryReader reader) evidence.Assertions.Add(ReadAssertion(reader)); else if (reader.IsStartElement(Saml2Constants.Elements.EncryptedAssertion, Saml2Constants.Namespace)) evidence.Assertions.Add(ReadAssertion(reader)); + else + break; } if (0 == evidence.AssertionIdReferences.Count diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index 0472551069..13acf4da86 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.ExceptionServices; using System.Security.Claims; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; @@ -1888,9 +1889,15 @@ internal IEnumerable GetContentEncryptionKeys(JwtSecurityToken jwtT return keys; var unwrappedKeys = new List(); - // keep track of exceptions thrown, keys that were tried - var exceptionStrings = new StringBuilder(); - var keysAttempted = new StringBuilder(); + + // Pre-generate a placeholder key used as a fallback when an unwrap call + // does not yield a usable key, so that downstream processing follows a + // single uniform path regardless of which key was tried. + int expectedCekSizeInBytes = GetExpectedCekSizeInBytes(jwtToken.Header.Enc); + byte[] fallbackCek = new byte[expectedCekSizeInBytes]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + rng.GetBytes(fallbackCek); + foreach (var key in keys) { try @@ -1940,15 +1947,39 @@ internal IEnumerable GetContentEncryptionKeys(JwtSecurityToken jwtT } catch (Exception ex) { - exceptionStrings.AppendLine(ex.ToString()); + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(ex.ToString()); + + // Use the placeholder key so the downstream code path is the + // same regardless of whether unwrap succeeded. + unwrappedKeys.Add(new SymmetricSecurityKey(fallbackCek)); } - keysAttempted.AppendLine(key.KeyId); } - if (unwrappedKeys.Count > 0 || exceptionStrings.Length == 0) - return unwrappedKeys; - else - throw LogHelper.LogExceptionMessage(new SecurityTokenKeyWrapException(LogHelper.FormatInvariant(TokenLogMessages.IDX10618, LogHelper.MarkAsNonPII(keysAttempted.ToString()), exceptionStrings, jwtToken))); + return unwrappedKeys; + } + + /// + /// Returns the expected Content Encryption Key (CEK) size in bytes for + /// the given content encryption algorithm (JWE "enc" header value). + /// + private static int GetExpectedCekSizeInBytes(string encAlgorithm) + { + if (SecurityAlgorithms.Aes128CbcHmacSha256.Equals(encAlgorithm, StringComparison.Ordinal)) + return 32; + if (SecurityAlgorithms.Aes192CbcHmacSha384.Equals(encAlgorithm, StringComparison.Ordinal)) + return 48; + if (SecurityAlgorithms.Aes256CbcHmacSha512.Equals(encAlgorithm, StringComparison.Ordinal)) + return 64; + + if (SecurityAlgorithms.Aes128Gcm.Equals(encAlgorithm, StringComparison.Ordinal)) + return 16; + if (SecurityAlgorithms.Aes192Gcm.Equals(encAlgorithm, StringComparison.Ordinal)) + return 24; + if (SecurityAlgorithms.Aes256Gcm.Equals(encAlgorithm, StringComparison.Ordinal)) + return 32; + + return 32; } private static byte[] GetSymmetricSecurityKey(SecurityKey key) diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptToken.UnwrapBehaviourTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptToken.UnwrapBehaviourTests.cs new file mode 100644 index 0000000000..21f410d529 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptToken.UnwrapBehaviourTests.cs @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + /// + /// Tests covering the unwrap-then-decrypt behaviour of . + /// When a key cannot produce a usable Content Encryption Key, the handler now uses a + /// placeholder key and proceeds to decryption. As a result, both unwrap-stage and + /// decryption-stage failures surface to callers as + /// (IDX10603) rather than + /// (IDX10618). + /// + public class JsonWebTokenHandlerDecryptTokenUnwrapBehaviourTests + { + /// + /// Happy path: a JWE produced and consumed with matching keys decrypts cleanly. + /// + /// + /// AES-GCM content encryption is not exercised here because the handler only + /// supports GCM with the direct-key alg, which bypasses the unwrap path + /// entirely. The GCM branches of the size lookup are defensive only. + /// + [Theory] + [InlineData(SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes128CbcHmacSha256)] + [InlineData(SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)] + [InlineData(SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes128CbcHmacSha256)] + public async Task ValidJwe_DecryptsSuccessfully(string alg, string enc) + { + var handler = new JsonWebTokenHandler(); + + var encryptingCredentials = new EncryptingCredentials( + KeyingMaterial.RsaSecurityKey_2048, alg, enc); + + string jwe = handler.CreateToken( + Default.PayloadString, + Default.SymmetricSigningCredentials, + encryptingCredentials); + + var tvp = Default.TokenValidationParameters( + KeyingMaterial.RsaSecurityKey_2048, + Default.SymmetricSigningKey256); + tvp.ValidateLifetime = false; + + var result = await handler.ValidateTokenAsync(jwe, tvp); + + Assert.True(result.IsValid, $"Token validation should succeed. Exception: {result.Exception}"); + } + + /// + /// A JWE whose encrypted-key segment has been altered must fail validation as a + /// decryption failure, not as a key-wrap failure. + /// + [Fact] + public async Task AlteredEncryptedKey_SurfacesAsDecryptionFailure() + { + var handler = new JsonWebTokenHandler(); + + var encryptingCredentials = new EncryptingCredentials( + KeyingMaterial.RsaSecurityKey_2048, + SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes128CbcHmacSha256); + + string jwe = handler.CreateToken( + Default.PayloadString, + Default.SymmetricSigningCredentials, + encryptingCredentials); + + string alteredJwe = AlterJweEncryptedKey(jwe, charactersToFlip: 2); + + var tvp = Default.TokenValidationParameters( + KeyingMaterial.RsaSecurityKey_2048, + Default.SymmetricSigningKey256); + tvp.ValidateLifetime = false; + + var result = await handler.ValidateTokenAsync(alteredJwe, tvp); + + Assert.False(result.IsValid, "Token validation should fail when the encrypted key is altered."); + Assert.NotNull(result.Exception); + Assert.IsNotType(result.Exception); + Assert.IsType(result.Exception); + } + + /// + /// A JWE decrypted with a different RSA key from the one used to encrypt it + /// must fail validation as a decryption failure, not as a key-wrap failure. + /// + [Fact] + public async Task MismatchedDecryptionKey_SurfacesAsDecryptionFailure() + { + var handler = new JsonWebTokenHandler(); + + var encryptingCredentials = new EncryptingCredentials( + KeyingMaterial.RsaSecurityKey_2048, + SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes128CbcHmacSha256); + + string jwe = handler.CreateToken( + Default.PayloadString, + Default.SymmetricSigningCredentials, + encryptingCredentials); + + // Different RSA key (also 2048-bit) so the failure is on key value, not size. + var tvp = Default.TokenValidationParameters( + KeyingMaterial.DefaultX509Key_2048, + Default.SymmetricSigningKey256); + tvp.ValidateLifetime = false; + + var result = await handler.ValidateTokenAsync(jwe, tvp); + + Assert.False(result.IsValid, "Token validation should fail when the decryption key does not match."); + Assert.NotNull(result.Exception); + Assert.IsNotType(result.Exception); + Assert.IsType(result.Exception); + } + + /// + /// Two distinct invalid inputs (an altered encrypted key vs a non-matching + /// decryption key) must surface to the caller through the same exception type. + /// + [Fact] + public async Task DistinctInvalidInputs_ProduceSameExceptionType() + { + var handler = new JsonWebTokenHandler(); + + var encryptingCredentials = new EncryptingCredentials( + KeyingMaterial.RsaSecurityKey_2048, + SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes256CbcHmacSha512); + + string jwe = handler.CreateToken( + Default.PayloadString, + Default.SymmetricSigningCredentials, + encryptingCredentials); + + string alteredJwe = AlterJweEncryptedKey(jwe, charactersToFlip: 3); + + var tvpA = Default.TokenValidationParameters( + KeyingMaterial.RsaSecurityKey_2048, + Default.SymmetricSigningKey256); + tvpA.ValidateLifetime = false; + + var tvpB = Default.TokenValidationParameters( + KeyingMaterial.DefaultX509Key_2048, + Default.SymmetricSigningKey256); + tvpB.ValidateLifetime = false; + + var resultA = await handler.ValidateTokenAsync(alteredJwe, tvpA); + var resultB = await handler.ValidateTokenAsync(jwe, tvpB); + + Assert.False(resultA.IsValid); + Assert.False(resultB.IsValid); + Assert.NotNull(resultA.Exception); + Assert.NotNull(resultB.Exception); + Assert.Equal(resultA.Exception.GetType(), resultB.Exception.GetType()); + Assert.IsType(resultA.Exception); + Assert.IsType(resultB.Exception); + } + + /// + /// When several keys are tried and only one is correct, decryption must succeed. + /// The placeholder key used for the non-matching attempt must not interfere. + /// + [Fact] + public async Task MultipleKeys_OneMatches_DecryptsCorrectly() + { + var handler = new JsonWebTokenHandler(); + + var encryptingCredentials = new EncryptingCredentials( + KeyingMaterial.RsaSecurityKey_2048, + SecurityAlgorithms.RsaOAEP, + SecurityAlgorithms.Aes128CbcHmacSha256); + + string jwe = handler.CreateToken( + Default.PayloadString, + Default.SymmetricSigningCredentials, + encryptingCredentials); + + var tvp = Default.TokenValidationParameters( + KeyingMaterial.RsaSecurityKey_2048, + Default.SymmetricSigningKey256); + tvp.TokenDecryptionKeys = new SecurityKey[] + { + KeyingMaterial.DefaultX509Key_2048, + KeyingMaterial.RsaSecurityKey_2048 + }; + tvp.ValidateLifetime = false; + + var result = await handler.ValidateTokenAsync(jwe, tvp); + + Assert.True(result.IsValid, $"Token validation should succeed when one of the supplied keys matches. Exception: {result.Exception}"); + } + + private static string AlterJweEncryptedKey(string jwe, int charactersToFlip) + { + string[] parts = jwe.Split('.'); + Assert.Equal(5, parts.Length); + + char[] altered = parts[1].ToCharArray(); + int count = Math.Min(charactersToFlip, altered.Length); + for (int i = 0; i < count; i++) + altered[i] = altered[i] == 'A' ? 'B' : 'A'; + + parts[1] = new string(altered); + return string.Join(".", parts); + } + } +} diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs index f39bcd1d48..db8337fe4f 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs @@ -228,7 +228,9 @@ static Dictionary AdditionalEcdhEsHeaderParameters(JsonWebKey pu }, new TokenDecryptingTheoryData("Invalid_AlgorithmMismatch_DecryptionFails") { - ExpectedException = ExpectedException.SecurityTokenKeyWrapException("IDX10618:"), + // Unwrap failures now surface as decryption failures (IDX10603) + // rather than key-wrap exceptions (IDX10618). + ExpectedException = ExpectedException.SecurityTokenDecryptionFailedException("IDX10603:"), SecurityTokenDescriptor = new SecurityTokenDescriptor { SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, @@ -239,11 +241,11 @@ static Dictionary AdditionalEcdhEsHeaderParameters(JsonWebKey pu decryptionKeys: [KeyingMaterial.RsaSecurityKey_2048]), OperationResult = new ValidationError( new MessageDetail( - TokenLogMessages.IDX10609, + TokenLogMessages.IDX10603, LogHelper.MarkAsSecurityArtifact( new JsonWebToken(ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims), JwtTokenUtilities.SafeLogJwtToken)), - ValidationFailureType.KeyWrapFailed, + ValidationFailureType.TokenDecryptionFailed, null), }, new TokenDecryptingTheoryData("KeyIdMismatch_TryAllDecryptionKeysTrue_DecryptionSucceeds") diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Decryption.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Decryption.cs index 2d6c54066c..f826377e80 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Decryption.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ValidateTokenAsyncTests.Decryption.cs @@ -81,9 +81,11 @@ public static TheoryData ValidateTokenAs TokenValidationParameters = CreateTokenValidationParameters(KeyingMaterial.DefaultRsaSecurityKey1), ValidationParameters = CreateValidationParameters(KeyingMaterial.DefaultRsaSecurityKey1), ExpectedIsValid = false, - ExpectedException = ExpectedException.SecurityTokenKeyWrapException("IDX10618:"), + // Unwrap failures now surface as decryption failures (IDX10603) + // rather than key-wrap exceptions (IDX10618). + ExpectedException = ExpectedException.SecurityTokenDecryptionFailedException("IDX10603:"), // Avoid comparing the full exception message as the stack traces for the inner exceptions are different. - ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenKeyWrapException("IDX10618:"), + ExpectedExceptionValidationParameters = ExpectedException.SecurityTokenDecryptionFailedException("IDX10603:"), }, new ValidateTokenAsyncDecryptionTheoryData("JWE_KeyWithKeyId_OnTokenDecryptFailure_KeysInConfig_SuccessOnRetry") { diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index f9aa459cb2..ad83c73871 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -674,7 +674,29 @@ public void GetEncryptionKeys(CreateTokenTheoryData theoryData) var jwtTokenFromJsonHandlerWithKid = new JsonWebToken(jweFromJsonHandlerWithKid); var encryptionKeysFromJsonHandlerWithKid = theoryData.JsonWebTokenHandler.GetContentEncryptionKeys(jwtTokenFromJsonHandlerWithKid, theoryData.ValidationParameters, theoryData.Configuration); - Assert.True(IdentityComparer.AreEqual(encryptionKeysFromJsonHandlerWithKid, theoryData.ExpectedDecryptionKeys)); + var actualKeys = encryptionKeysFromJsonHandlerWithKid.ToList(); + + foreach (var expectedKey in theoryData.ExpectedDecryptionKeys) + { + if (jwtTokenFromJsonHandlerWithKid.Alg.Equals(JwtConstants.DirectKeyUseAlg, StringComparison.Ordinal) + || jwtTokenFromJsonHandlerWithKid.Alg.Equals(SecurityAlgorithms.EcdhEs, StringComparison.Ordinal)) + { + // Direct key use — keys are returned as-is, exact match. + Assert.Contains(expectedKey, actualKeys); + } + else + { + // Key wrapping — returned keys are unwrapped CEKs. + // Verify the correct CEK is present by unwrapping ourselves. + if (expectedKey.CryptoProviderFactory.IsSupportedAlgorithm(jwtTokenFromJsonHandlerWithKid.Alg, expectedKey)) + { + var kwp = expectedKey.CryptoProviderFactory.CreateKeyWrapProviderForUnwrap(expectedKey, jwtTokenFromJsonHandlerWithKid.Alg); + var expectedCekBytes = kwp.UnwrapKey(jwtTokenFromJsonHandlerWithKid.EncryptedKeyBytes); + var filteredKeys = actualKeys.Where(k => k is SymmetricSecurityKey sk && sk.Key.SequenceEqual(expectedCekBytes)); + Assert.Single(filteredKeys); + } + } + } theoryData.ExpectedException.ProcessNoException(context); } catch (Exception ex) diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs index 47da4c6bba..4e11ab3660 100644 --- a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SerializerTests.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Text; +using System.Threading.Tasks; using System.Xml; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Xml; @@ -465,10 +466,49 @@ public static TheoryData ReadEvidenceTheoryData ExpectedException = new ExpectedException(typeof(Saml2SecurityTokenReadException), "IDX13122"), Saml2Serializer = new Saml2SerializerPublic(), TestId = "Saml2EvidenceEmpty" + }, + new Saml2TheoryData + { + // Element from a foreign namespace as the only child — not part of the SAML 2.0 EvidenceType choice. + Xml = "", + ExpectedException = new ExpectedException(typeof(Saml2SecurityTokenReadException), "IDX13122"), + Saml2Serializer = new Saml2SerializerPublic(), + TestId = "Saml2EvidenceUnexpectedChildForeignNamespace" + }, + new Saml2TheoryData + { + // Valid AssertionIDRef followed by an element not in the choice — must surface as a read exception, not silently succeed. + Xml = "_abc", + ExpectedException = new ExpectedException(typeof(Saml2SecurityTokenReadException), "IDX13102", ignoreInnerException: true), + Saml2Serializer = new Saml2SerializerPublic(), + TestId = "Saml2EvidenceValidContentThenUnexpectedChild" } }; } } + + // Bounds the work performed when reading an Evidence element with content outside the + // declared choice. Runs on a worker task with a small budget so the assertion is reached + // even if the serializer fails to make progress. + [Fact] + public async Task ReadEvidenceCompletesOnUnexpectedChildElements() + { + const string xml = + "" + + "" + + ""; + + var serializer = new Saml2SerializerPublic(); + var task = Task.Run(() => + { + var reader = XmlUtilities.CreateDictionaryReader(xml); + Assert.Throws(() => serializer.ReadEvidencePublic(reader)); + }); + + var completed = await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(1))); + Assert.Same(task, completed); + await task; + } #endregion #region Saml2ProxyRestriction diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSerializerBoundsTests.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSerializerBoundsTests.cs new file mode 100644 index 0000000000..e45b06c91c --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSerializerBoundsTests.cs @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Text; +using System.Xml; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Saml2; +using Xunit; + +#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant + +namespace Microsoft.IdentityModel.Tokens.Saml.Tests +{ + public class SamlSerializerBoundsTests + { + // Bound enforced by SamlSerializer / Saml2Serializer (private MaxDepth = 8). + // Tests reference the value indirectly via theory data: [8, 9, 10] (at-or-above) + // and [1, 7] (lower edge and bound - 1). + + #region SAML2 nested-assertion bounds + + [Theory] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + public void Saml2_ReadAssertion_AtOrAboveBound_ReportsBoundExceeded(int depth) + { + var serializer = new Saml2Serializer(); + string xml = BuildNestedSaml2Xml(depth); + var reader = XmlReader.Create(new StringReader(xml)); + + var ex = Assert.Throws(() => serializer.ReadAssertion(reader)); + Assert.Contains("IDX13111", ex.Message); + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + public void Saml2_ReadAssertion_WithinBound_DoesNotReportBoundExceeded(int depth) + { + var serializer = new Saml2Serializer(); + string xml = BuildNestedSaml2Xml(depth); + var reader = XmlReader.Create(new StringReader(xml)); + + try + { + serializer.ReadAssertion(reader); + } + catch (Exception ex) + { + // Other parse exceptions are acceptable — only verify the nesting + // bound check does not fire when the input is within the bound. + Assert.DoesNotContain("IDX13111", ex.Message); + } + } + + [Fact] + public void Saml2_ReadAssertion_KnownGoodAssertion_ParsesSuccessfully() + { + var serializer = new Saml2Serializer(); + var reader = XmlReader.Create(new StringReader(ReferenceXml.Saml2Valid)); + + var assertion = serializer.ReadAssertion(reader); + + Assert.NotNull(assertion); + Assert.NotNull(assertion.Issuer); + Assert.False(string.IsNullOrEmpty(assertion.Issuer.Value)); + } + + [Fact] + public void Saml2_ReadAssertion_NestedAtBoundMinusOne_ParsesSuccessfully() + { + // depth = 7 (one below the bound). All 7 nested assertions must parse + // and the bound check must not fire. + var serializer = new Saml2Serializer(); + string xml = BuildNestedSaml2Xml(7); + var reader = XmlReader.Create(new StringReader(xml)); + + var assertion = serializer.ReadAssertion(reader); + + Assert.NotNull(assertion); + Assert.NotNull(assertion.Advice); + } + + [Fact] + public void Saml2_ReadAssertion_StateResetsAfterFailure_KnownGoodStillParses() + { + var serializer = new Saml2Serializer(); + + // Trigger the bound on the first call. + string deepXml = BuildNestedSaml2Xml(10); + var reader1 = XmlReader.Create(new StringReader(deepXml)); + var ex = Assert.Throws(() => serializer.ReadAssertion(reader1)); + Assert.Contains("IDX13111", ex.Message); + + // The same serializer instance must still parse a known-good assertion. + // This both proves state reset and catches any failure to decrement on the throw path. + var reader2 = XmlReader.Create(new StringReader(ReferenceXml.Saml2Valid)); + var assertion = serializer.ReadAssertion(reader2); + + Assert.NotNull(assertion); + Assert.NotNull(assertion.Issuer); + } + + [Fact] + public void Saml2_ReadAssertion_StateResetsAfterSuccess_DeepInputStillRejected() + { + // After a successful parse the bound counter must be back to zero so + // a subsequent deep input is still rejected at the expected depth. + var serializer = new Saml2Serializer(); + + var reader1 = XmlReader.Create(new StringReader(ReferenceXml.Saml2Valid)); + Assert.NotNull(serializer.ReadAssertion(reader1)); + + var reader2 = XmlReader.Create(new StringReader(BuildNestedSaml2Xml(10))); + var ex = Assert.Throws(() => serializer.ReadAssertion(reader2)); + Assert.Contains("IDX13111", ex.Message); + } + + #endregion + + #region SAML1 nested-assertion bounds + + [Theory] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + public void Saml1_ReadAssertion_AtOrAboveBound_ReportsBoundExceeded(int depth) + { + var serializer = new SamlSerializer(); + string xml = BuildNestedSaml1Xml(depth); + var reader = XmlReader.Create(new StringReader(xml)); + + var ex = Assert.Throws(() => serializer.ReadAssertion(reader)); + Assert.Contains("IDX11138", ex.Message); + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + public void Saml1_ReadAssertion_WithinBound_DoesNotReportBoundExceeded(int depth) + { + var serializer = new SamlSerializer(); + string xml = BuildNestedSaml1Xml(depth); + var reader = XmlReader.Create(new StringReader(xml)); + + try + { + serializer.ReadAssertion(reader); + } + catch (Exception ex) + { + Assert.DoesNotContain("IDX11138", ex.Message); + } + } + + [Fact] + public void Saml1_ReadAssertion_KnownGoodAssertion_ParsesSuccessfully() + { + var serializer = new SamlSerializer(); + var reader = XmlReader.Create(new StringReader(ReferenceTokens.SamlToken_Valid)); + + var assertion = serializer.ReadAssertion(reader); + + Assert.NotNull(assertion); + Assert.False(string.IsNullOrEmpty(assertion.Issuer)); + } + + [Fact] + public void Saml1_ReadAssertion_NestedAtBoundMinusOne_ParsesSuccessfully() + { + var serializer = new SamlSerializer(); + string xml = BuildNestedSaml1Xml(7); + var reader = XmlReader.Create(new StringReader(xml)); + + var assertion = serializer.ReadAssertion(reader); + + Assert.NotNull(assertion); + Assert.NotEmpty(assertion.Advice.Assertions); + } + + [Fact] + public void Saml1_ReadAssertion_StateResetsAfterFailure_KnownGoodStillParses() + { + var serializer = new SamlSerializer(); + + string deepXml = BuildNestedSaml1Xml(10); + var reader1 = XmlReader.Create(new StringReader(deepXml)); + var ex = Assert.Throws(() => serializer.ReadAssertion(reader1)); + Assert.Contains("IDX11138", ex.Message); + + var reader2 = XmlReader.Create(new StringReader(ReferenceTokens.SamlToken_Valid)); + var assertion = serializer.ReadAssertion(reader2); + + Assert.NotNull(assertion); + } + + [Fact] + public void Saml1_ReadAssertion_StateResetsAfterSuccess_DeepInputStillRejected() + { + var serializer = new SamlSerializer(); + + var reader1 = XmlReader.Create(new StringReader(ReferenceTokens.SamlToken_Valid)); + Assert.NotNull(serializer.ReadAssertion(reader1)); + + var reader2 = XmlReader.Create(new StringReader(BuildNestedSaml1Xml(10))); + var ex = Assert.Throws(() => serializer.ReadAssertion(reader2)); + Assert.Contains("IDX11138", ex.Message); + } + + #endregion + + #region Reader quotas + + [Fact] + public void BoundedXmlDictionaryReaderQuotas_HasExpectedMaxDepth() + { + // Regression: the bounded MaxDepth is the contract these handlers rely on. + Assert.Equal(32, BoundedXmlDictionaryReaderQuotas.Quotas.MaxDepth); + } + + [Fact] + public void BoundedXmlDictionaryReaderQuotas_RejectsXmlBeyondMaxDepth() + { + // Using the bounded quotas, an XML element chain deeper than MaxDepth + // must be rejected by the reader with an XmlException mentioning MaxDepth. + string deepXml = BuildSimpleNestedXml(depth: BoundedXmlDictionaryReaderQuotas.Quotas.MaxDepth + 5); + var bytes = Encoding.UTF8.GetBytes(deepXml); + + using var reader = XmlDictionaryReader.CreateTextReader(bytes, BoundedXmlDictionaryReaderQuotas.Quotas); + + var ex = Assert.Throws(() => + { + while (reader.Read()) + { + } + }); + Assert.Contains("MaxDepth", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void BoundedXmlDictionaryReaderQuotas_AcceptsXmlAtMaxDepth() + { + // Sanity: nesting up to MaxDepth must read cleanly. Catches any off-by-one + // regression in the bound itself. + string xml = BuildSimpleNestedXml(depth: BoundedXmlDictionaryReaderQuotas.Quotas.MaxDepth); + var bytes = Encoding.UTF8.GetBytes(xml); + + using var reader = XmlDictionaryReader.CreateTextReader(bytes, BoundedXmlDictionaryReaderQuotas.Quotas); + while (reader.Read()) + { + } + } + + private static string BuildSimpleNestedXml(int depth) + { + var sb = new StringBuilder(); + for (int i = 0; i < depth; i++) + sb.Append(""); + for (int i = 0; i < depth; i++) + sb.Append(""); + return sb.ToString(); + } + + #endregion + + #region Helpers + + /// + /// Builds a SAML 2.0 assertion XML fragment with the requested nesting depth. + /// Depth 1 = single assertion; depth 2 = assertion containing one nested assertion via Advice; and so on. + /// + private static string BuildNestedSaml2Xml(int depth) + { + const string ns = "urn:oasis:names:tc:SAML:2.0:assertion"; + var sb = new StringBuilder(); + + for (int i = 0; i < depth; i++) + { + sb.AppendFormat( + "", + i, ns); + sb.Append("http://issuer.com"); + sb.Append("user@example.com"); + if (i < depth - 1) + sb.Append(""); + } + + for (int i = depth - 1; i >= 0; i--) + { + sb.Append(""); + if (i > 0) + sb.Append(""); + } + + return sb.ToString(); + } + + /// + /// Builds a SAML 1.1 assertion XML fragment with the requested nesting depth. + /// + private static string BuildNestedSaml1Xml(int depth) + { + const string ns = "urn:oasis:names:tc:SAML:1.0:assertion"; + var sb = new StringBuilder(); + + for (int i = 0; i < depth; i++) + { + sb.AppendFormat( + "", + i, ns); + if (i < depth - 1) + sb.Append(""); + } + + AppendSaml1Statement(sb); + sb.Append(""); + + for (int i = depth - 2; i >= 0; i--) + { + sb.Append(""); + AppendSaml1Statement(sb); + sb.Append(""); + } + + return sb.ToString(); + } + + private static void AppendSaml1Statement(StringBuilder sb) + { + sb.Append(""); + sb.Append("user@example.com"); + sb.Append(""); + sb.Append("admin"); + sb.Append(""); + sb.Append(""); + } + + #endregion + } +} diff --git a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs index 50e877ada7..64265f7ed7 100644 --- a/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs +++ b/test/System.IdentityModel.Tokens.Jwt.Tests/JwtSecurityTokenHandlerTests.cs @@ -3143,28 +3143,6 @@ public static TheoryData SecurityTokenDecryptionTheoryDat ExpectedDecryptionKeys = new List(){ KeyingMaterial.DefaultSymmetricSecurityKey_256 }, Algorithm = JwtConstants.DirectKeyUseAlg, EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2_NoKeyId - }, - new CreateTokenTheoryData - { - TestId = "AlgorithmMisMatch", - Payload = Default.PayloadString, - ExpectedException = ExpectedException.KeyWrapException("IDX10618:"), - TokenDescriptor = new SecurityTokenDescriptor - { - SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials, - EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2, - Claims = Default.PayloadDictionary - }, - JwtSecurityTokenHandler = new JwtSecurityTokenHandler(), - ValidationParameters = new TokenValidationParameters - { - IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key, - TokenDecryptionKeys = new List(){ KeyingMaterial.DefaultSymmetricSecurityKey_256 }, - ValidAudience = Default.Audience, - ValidIssuer = Default.Issuer - }, - Algorithm = SecurityAlgorithms.Aes256CbcHmacSha512, - EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2_NoKeyId } }; }