Skip to content

Commit

Permalink
SAML new model validation: Signature (#2958)
Browse files Browse the repository at this point in the history
* Added XmlValidationError. Added ValidationError property to XmlValidationException to provide custom stack traces

* Added alternative versions using ValidationParameters to XML signature validations

* Added XmlValidationFailure to ValidationFailureType

* Added refactored ValidateSignature method to SamlSecurityTokenHandler.
Updated ValidateTokenAsync to call ValidateSignature.

* Added tests to compare signature validation between the legacy and new path

* Re-added API lost in merge to InternalAPI.Unshipped.txt
  • Loading branch information
iNinja authored Nov 2, 2024
1 parent ba49516 commit 5471249
Show file tree
Hide file tree
Showing 16 changed files with 649 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateTokenAsync(
Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames
Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.ValidateTokenAsync(Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.IssuerValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.SignatureValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.ValidateSignature(Microsoft.IdentityModel.Tokens.Saml.SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.SecurityKey>
static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.PopulateValidationParametersWithCurrentConfigurationAsync(Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationParameters>
Microsoft.IdentityModel.Tokens.Saml2.SamlSecurityTokenHandler.ValidateTokenAsync(SamlSecurityToken samlToken, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.CallContext callContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.IdentityModel.Tokens.ValidationResult<Microsoft.IdentityModel.Tokens.ValidatedToken>>
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame
Expand All @@ -20,6 +22,7 @@ static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.OneTimeUseValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlSecurityTokenHandler.StackFrames.TokenValidationParametersNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml.SamlTokenUtilities.ResolveTokenSigningKey(Microsoft.IdentityModel.Xml.KeyInfo tokenKeyInfo, Microsoft.IdentityModel.Tokens.ValidationParameters validationParameters) -> Microsoft.IdentityModel.Tokens.SecurityKey
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsNull -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionConditionsValidationFailed -> System.Diagnostics.StackFrame
static Microsoft.IdentityModel.Tokens.Saml2.Saml2SecurityTokenHandler.StackFrames.AssertionNull -> System.Diagnostics.StackFrame
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Microsoft.IdentityModel.Xml;
using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;

#nullable enable
namespace Microsoft.IdentityModel.Tokens.Saml
{
public partial class SamlSecurityTokenHandler : SecurityTokenHandler
{
internal static ValidationResult<SecurityKey> ValidateSignature(
SamlSecurityToken samlToken,
ValidationParameters validationParameters,
#pragma warning disable CA1801 // Review unused parameters
CallContext callContext)
#pragma warning restore CA1801 // Review unused parameters
{
if (samlToken is null)
{
return ValidationError.NullParameter(
nameof(samlToken),
new StackFrame(true));
}

if (validationParameters is null)
{
return ValidationError.NullParameter(
nameof(validationParameters),
new StackFrame(true));
}

// Delegate is set by the user, we call it and return the result.
if (validationParameters.SignatureValidator is not null)
return validationParameters.SignatureValidator(samlToken, validationParameters, null, callContext);

// If the user wants to accept unsigned tokens, they must implement the delegate
if (samlToken.Assertion.Signature is null)
return new XmlValidationError(
new MessageDetail(
TokenLogMessages.IDX10504,
samlToken.Assertion.CanonicalString),
ValidationFailureType.SignatureValidationFailed,
typeof(SecurityTokenValidationException),
new StackFrame(true));

IList<SecurityKey>? keys = null;
SecurityKey? resolvedKey = null;
bool keyMatched = false;

if (validationParameters.IssuerSigningKeyResolver is not null)
{
resolvedKey = validationParameters.IssuerSigningKeyResolver(
samlToken.Assertion.CanonicalString,
samlToken,
samlToken.Assertion.Signature.KeyInfo?.Id,
validationParameters,
null,
callContext);
}
else
{
resolvedKey = SamlTokenUtilities.ResolveTokenSigningKey(samlToken.Assertion.Signature.KeyInfo, validationParameters);
}

if (resolvedKey is null)
{
if (validationParameters.TryAllIssuerSigningKeys)
keys = validationParameters.IssuerSigningKeys;
}
else
{
keys = [resolvedKey];
keyMatched = true;
}

bool canMatchKey = samlToken.Assertion.Signature.KeyInfo != null;
List<ValidationError> errors = new();
StringBuilder keysAttempted = new();

if (keys is not null)
{
for (int i = 0; i < keys.Count; i++)
{
SecurityKey key = keys[i];
ValidationResult<string> algorithmValidationResult = validationParameters.AlgorithmValidator(
samlToken.Assertion.Signature.SignedInfo.SignatureMethod,
key,
samlToken,
validationParameters,
callContext);

if (!algorithmValidationResult.IsValid)
{
errors.Add(algorithmValidationResult.UnwrapError());
}
else
{
var validationError = samlToken.Assertion.Signature.Verify(
key,
validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory,
callContext);

if (validationError is null)
{
samlToken.SigningKey = key;

return key;
}
else
{
errors.Add(validationError.AddStackFrame(new StackFrame()));
}
}

keysAttempted.Append(key.ToString());
if (canMatchKey && !keyMatched && key.KeyId is not null && samlToken.Assertion.Signature.KeyInfo is not null)
keyMatched = samlToken.Assertion.Signature.KeyInfo.MatchesKey(key);
}
}

if (canMatchKey && keyMatched)
return new XmlValidationError(
new MessageDetail(
TokenLogMessages.IDX10514,
keysAttempted.ToString(),
samlToken.Assertion.Signature.KeyInfo,
GetErrorStrings(errors),
samlToken),
ValidationFailureType.SignatureValidationFailed,
typeof(SecurityTokenInvalidSignatureException),
new StackFrame(true));

if (keysAttempted.Length > 0)
return new XmlValidationError(
new MessageDetail(
TokenLogMessages.IDX10512,
keysAttempted.ToString(),
GetErrorStrings(errors),
samlToken),
ValidationFailureType.SignatureValidationFailed,
typeof(SecurityTokenSignatureKeyNotFoundException),
new StackFrame(true));

return new XmlValidationError(
new MessageDetail(TokenLogMessages.IDX10500),
ValidationFailureType.SignatureValidationFailed,
typeof(SecurityTokenSignatureKeyNotFoundException),
new StackFrame(true));
}

private static string GetErrorStrings(List<ValidationError> errors)
{
StringBuilder sb = new();
for (int i = 0; i < errors.Count; i++)
{
sb.AppendLine(errors[i].ToString());
}

return sb.ToString();
}
}
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ internal async Task<ValidationResult<ValidatedToken>> ValidateTokenAsync(
return issuerValidationResult.UnwrapError().AddStackFrame(StackFrames.IssuerValidationFailed);
}

var signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext);
if (!signatureValidationResult.IsValid)
{
StackFrames.SignatureValidationFailed ??= new StackFrame(true);
return signatureValidationResult.UnwrapError().AddStackFrame(StackFrames.SignatureValidationFailed);
}

return new ValidatedToken(samlToken, this, validationParameters);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal static class StackFrames
internal static StackFrame? OneTimeUseValidationFailed;

internal static StackFrame? IssuerValidationFailed;
internal static StackFrame? SignatureValidationFailed;
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlTokenUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ internal static SecurityKey ResolveTokenSigningKey(KeyInfo tokenKeyInfo, TokenVa
return null;
}

/// <summary>
/// Returns a <see cref="SecurityKey"/> to use when validating the signature of a token.
/// </summary>
/// <param name="tokenKeyInfo">The <see cref="KeyInfo"/> field of the token being validated</param>
/// <param name="validationParameters">The <see cref="ValidationParameters"/> to be used for validating the token.</param>
/// <returns>Returns a <see cref="SecurityKey"/> to use for signature validation.</returns>
/// <remarks>If key fails to resolve, then null is returned</remarks>
internal static SecurityKey ResolveTokenSigningKey(KeyInfo tokenKeyInfo, ValidationParameters validationParameters)
{
if (tokenKeyInfo is null || validationParameters.IssuerSigningKeys is null)
return null;

for (int i = 0; i < validationParameters.IssuerSigningKeys.Count; i++)
{
if (tokenKeyInfo.MatchesKey(validationParameters.IssuerSigningKeys[i]))
return validationParameters.IssuerSigningKeys[i];
}

return null;
}



/// <summary>
/// Creates <see cref="Claim"/>'s from <paramref name="claimsCollection"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedStr
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoTokenAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoValidationParameterAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAlgorithmValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.XmlValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ internal Exception GetException(Type exceptionType, Exception innerException)
exception = new SecurityTokenException(MessageDetail.Message);
else if (exceptionType == typeof(SecurityTokenKeyWrapException))
exception = new SecurityTokenKeyWrapException(MessageDetail.Message);
else if (ExceptionType == typeof(SecurityTokenValidationException))
exception = new SecurityTokenValidationException(MessageDetail.Message);
else
{
// Exception type is unknown
Expand Down Expand Up @@ -175,6 +177,8 @@ internal Exception GetException(Type exceptionType, Exception innerException)
exception = new SecurityTokenException(MessageDetail.Message, actualException);
else if (exceptionType == typeof(SecurityTokenKeyWrapException))
exception = new SecurityTokenKeyWrapException(MessageDetail.Message, actualException);
else if (exceptionType == typeof(SecurityTokenValidationException))
exception = new SecurityTokenValidationException(MessageDetail.Message, actualException);
else
{
// Exception type is unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,11 @@ private class TokenDecryptionFailure : ValidationFailureType { internal TokenDec
/// </summary>
public static readonly ValidationFailureType InvalidSecurityToken = new InvalidSecurityTokenFailure("InvalidSecurityToken");
private class InvalidSecurityTokenFailure : ValidationFailureType { internal InvalidSecurityTokenFailure(string name) : base(name) { } }

/// <summary>
/// Defines a type that represents that an XML validation failed.
/// </summary>
public static readonly ValidationFailureType XmlValidationFailed = new XmlValidationFailure("XmlValidationFailed");
private class XmlValidationFailure : ValidationFailureType { internal XmlValidationFailure(string name) : base(name) { } }
}
}
34 changes: 34 additions & 0 deletions src/Microsoft.IdentityModel.Xml/Exceptions/XmlValidationError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Xml
{
internal class XmlValidationError : ValidationError
{
public XmlValidationError(
MessageDetail messageDetail,
ValidationFailureType validationFailureType,
Type exceptionType,
StackFrame stackFrame) :
base(messageDetail, validationFailureType, exceptionType, stackFrame)
{

}

internal override Exception GetException()
{
if (ExceptionType == typeof(XmlValidationException))
{
XmlValidationException exception = new(MessageDetail.Message, InnerException);
exception.SetValidationError(this);
return exception;
}

return base.GetException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Runtime.Serialization;
#pragma warning disable IDE0005 // Using directive is unnecessary.
using System.Text;
#pragma warning restore IDE0005 // Using directive is unnecessary.
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Xml
{
Expand All @@ -12,6 +17,11 @@ namespace Microsoft.IdentityModel.Xml
[Serializable]
public class XmlValidationException : XmlException
{
[NonSerialized]
private string _stackTrace;

private ValidationError _validationError;

/// <summary>
/// Initializes a new instance of the <see cref="XmlValidationException"/> class.
/// </summary>
Expand Down Expand Up @@ -49,5 +59,43 @@ protected XmlValidationException(SerializationInfo info, StreamingContext contex
: base(info, context)
{
}

/// <summary>
/// Sets the <see cref="ValidationError"/> that caused the exception.
/// </summary>
/// <param name="validationError"></param>
internal void SetValidationError(ValidationError validationError)
{
_validationError = validationError;
}

/// <summary>
/// Gets the stack trace that is captured when the exception is created.
/// </summary>
public override string StackTrace
{
get
{
if (_stackTrace == null)
{
if (_validationError == null)
return base.StackTrace;
#if NET8_0_OR_GREATER
_stackTrace = new StackTrace(_validationError.StackFrames).ToString();
#else
StringBuilder sb = new();
foreach (StackFrame frame in _validationError.StackFrames)
{
sb.Append(frame.ToString());
sb.Append(Environment.NewLine);
}

_stackTrace = sb.ToString();
#endif
}

return _stackTrace;
}
}
}
}
7 changes: 7 additions & 0 deletions src/Microsoft.IdentityModel.Xml/InternalAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Microsoft.IdentityModel.Xml.Reference.Verify(Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError
Microsoft.IdentityModel.Xml.Signature.Verify(Microsoft.IdentityModel.Tokens.SecurityKey key, Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError
Microsoft.IdentityModel.Xml.SignedInfo.Verify(Microsoft.IdentityModel.Tokens.CryptoProviderFactory cryptoProviderFactory, Microsoft.IdentityModel.Tokens.CallContext callContext) -> Microsoft.IdentityModel.Tokens.ValidationError
Microsoft.IdentityModel.Xml.XmlValidationError
Microsoft.IdentityModel.Xml.XmlValidationError.XmlValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame) -> void
Microsoft.IdentityModel.Xml.XmlValidationException.SetValidationError(Microsoft.IdentityModel.Tokens.ValidationError validationError) -> void
override Microsoft.IdentityModel.Xml.XmlValidationError.GetException() -> System.Exception
1 change: 1 addition & 0 deletions src/Microsoft.IdentityModel.Xml/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
override Microsoft.IdentityModel.Xml.XmlValidationException.StackTrace.get -> string
Loading

0 comments on commit 5471249

Please sign in to comment.