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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -137,9 +138,15 @@ internal ValidationResult<string, ValidationError> DecryptToken(
return (keys, null); // Cannot iterate over null.

var unwrappedKeys = new List<SecurityKey>();
// 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];
Expand Down Expand Up @@ -182,27 +189,16 @@ internal ValidationResult<string, ValidationError> 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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1332,9 +1333,15 @@ internal IEnumerable<SecurityKey> GetContentEncryptionKeys(JsonWebToken jwtToken
return keys;

var unwrappedKeys = new List<SecurityKey>();
// 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)
Expand Down Expand Up @@ -1387,22 +1394,43 @@ internal IEnumerable<SecurityKey> 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;
}

/// <summary>
/// Returns the expected Content Encryption Key (CEK) size in bytes for
/// the given content encryption algorithm (JWE "enc" header value).
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Xml;

namespace Microsoft.IdentityModel.Tokens.Saml
{
/// <summary>
/// Provides <see cref="XmlDictionaryReaderQuotas"/> with a bounded <see cref="XmlDictionaryReaderQuotas.MaxDepth"/>
/// suitable for parsing XML received from external sources.
/// </summary>
internal static class BoundedXmlDictionaryReaderQuotas
{
/// <summary>
/// Bounded quotas with <see cref="XmlDictionaryReaderQuotas.MaxDepth"/> = 32. All other limits match <see cref="XmlDictionaryReaderQuotas.Max"/>.
/// </summary>
internal static XmlDictionaryReaderQuotas Quotas { get; } = new XmlDictionaryReaderQuotas
{
MaxDepth = 32,
MaxStringContentLength = int.MaxValue,
MaxArrayLength = int.MaxValue,
MaxBytesPerRead = int.MaxValue,
MaxNameTableCharCount = int.MaxValue
};
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -36,7 +36,7 @@ internal virtual ValidationResult<SecurityToken, ValidationError> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace Microsoft.IdentityModel.Tokens.Saml
/// </summary>
public class SamlSerializer
{
private const int MaxDepth = 8;
[ThreadStatic]
private static int t_currentDepth;
private DSigSerializer _dsigSerializer = DSigSerializer.Default;
private string _prefix = SamlConstants.Prefix;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -292,6 +302,10 @@ public virtual SamlAssertion ReadAssertion(XmlReader reader)

throw LogReadException(LogMessages.IDX11122, ex, SamlConstants.Elements.Assertion, ex);
}
finally
{
t_currentDepth--;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,7 +34,7 @@ internal virtual ValidationResult<SecurityToken, ValidationError> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <saml:EncryptedAttribute> was encountered while processing the attribute statement.To handle encrypted attributes, extend the Saml2SecurityTokenHandler and override ReadAttributeStatement.";
internal const string IDX13118 = "IDX13118: A <saml:AuthnContextDecl> 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}'";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace Microsoft.IdentityModel.Tokens.Saml2
/// </summary>
public class Saml2Serializer
{
private const int MaxDepth = 8;
[ThreadStatic]
private static int t_currentDepth;
private DSigSerializer _dsigSerializer = DSigSerializer.Default;
private string _prefix = Saml2Constants.Prefix;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -282,6 +292,10 @@ public virtual Saml2Assertion ReadAssertion(XmlReader reader)

throw LogReadException(LogMessages.IDX13102, ex, Saml2Constants.Elements.Assertion, ex);
}
finally
{
t_currentDepth--;
}
}

/// <summary>
Expand Down Expand Up @@ -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
Expand Down
Loading