Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAML new model validation: Signature #2958

Merged
merged 10 commits into from
Nov 2, 2024
Merged
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
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) { } }
}
}
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
Loading