From 751174b741bb3ebdc0042e9e46b62e1bbbc55efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20T=C3=B8nners?= Date: Wed, 31 Aug 2016 14:14:59 +0200 Subject: [PATCH 1/6] Add support for different signing algorithms per identityprovider Tested with SAML2 Redirect and POST bindings --- Kentor.AuthServices/IdentityProvider.cs | 9 +- .../Kentor.AuthServices.csproj | 2 + .../ManagedSha256SignatureDescription.cs | 5 - .../MessageSigningAlgorithm.cs | 26 +++++ Kentor.AuthServices/SAML2P/ISaml2Message.cs | 6 + .../SAML2P/Saml2RequestBase.cs | 1 + Kentor.AuthServices/SAML2P/Saml2Response.cs | 2 + .../SAML2P/Saml2StatusResponseType.cs | 2 + .../WebSSO/Saml2PostBinding.cs | 31 +++--- .../WebSSO/Saml2RedirectBinding.cs | 92 +++++----------- .../XmlDocumentSigningExtensions.cs | 103 ++++++++++++++++++ 11 files changed, 193 insertions(+), 86 deletions(-) create mode 100644 Kentor.AuthServices/MessageSigningAlgorithm.cs create mode 100644 Kentor.AuthServices/XmlDocumentSigningExtensions.cs diff --git a/Kentor.AuthServices/IdentityProvider.cs b/Kentor.AuthServices/IdentityProvider.cs index ccb9a5c4f..0ba2c141a 100644 --- a/Kentor.AuthServices/IdentityProvider.cs +++ b/Kentor.AuthServices/IdentityProvider.cs @@ -290,7 +290,8 @@ public string MetadataLocation // For now we only support one attribute consuming service. AttributeConsumingServiceIndex = spOptions.AttributeConsumingServices.Any() ? 0 : (int?)null, NameIdPolicy = spOptions.NameIdPolicy, - RequestedAuthnContext = spOptions.RequestedAuthnContext + RequestedAuthnContext = spOptions.RequestedAuthnContext, + SigningAlgorithm = SigningAlgorithm }; if (spOptions.AuthenticateRequestSigningBehavior == SigningBehavior.Always @@ -307,11 +308,17 @@ public string MetadataLocation } authnRequest.SigningCertificate = spOptions.SigningServiceCertificate; + authnRequest.SigningAlgorithm = this.SigningAlgorithm; } return authnRequest; } + /// + /// Signing Algorithm to be used when signing the Authentication Request + /// + public MessageSigningAlgorithm SigningAlgorithm { get; set; } = MessageSigningAlgorithm.RsaSecureHashAlgorithm1; + /// /// Bind a Saml2AuthenticateRequest using the active binding of the idp, /// producing a CommandResult with the result of the binding. diff --git a/Kentor.AuthServices/Kentor.AuthServices.csproj b/Kentor.AuthServices/Kentor.AuthServices.csproj index 717e0a941..76b420bed 100644 --- a/Kentor.AuthServices/Kentor.AuthServices.csproj +++ b/Kentor.AuthServices/Kentor.AuthServices.csproj @@ -68,6 +68,7 @@ + @@ -170,6 +171,7 @@ + diff --git a/Kentor.AuthServices/ManagedSha256SignatureDescription.cs b/Kentor.AuthServices/ManagedSha256SignatureDescription.cs index 8888889ec..7aeeee062 100644 --- a/Kentor.AuthServices/ManagedSha256SignatureDescription.cs +++ b/Kentor.AuthServices/ManagedSha256SignatureDescription.cs @@ -1,11 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Security.Cryptography; -using System.Security.Cryptography.Xml; -using System.Text; -using System.Threading.Tasks; namespace Kentor.AuthServices { diff --git a/Kentor.AuthServices/MessageSigningAlgorithm.cs b/Kentor.AuthServices/MessageSigningAlgorithm.cs new file mode 100644 index 000000000..c2acc02f9 --- /dev/null +++ b/Kentor.AuthServices/MessageSigningAlgorithm.cs @@ -0,0 +1,26 @@ +namespace Kentor.AuthServices.Saml2P +{ + /// + /// Enum MessageSigningAlgorithm + /// + public enum MessageSigningAlgorithm + { + /// + /// The rsasha1 + /// + RsaSecureHashAlgorithm1, + /// + /// The rsasha256 + /// + RsaSecureHashAlgorithm256, + /// + /// The rsasha384 + /// + RsaSecureHashAlgorithm384, + /// + /// The rsasha512 + /// + RsaSecureHashAlgorithm512 + + } +} \ No newline at end of file diff --git a/Kentor.AuthServices/SAML2P/ISaml2Message.cs b/Kentor.AuthServices/SAML2P/ISaml2Message.cs index 8293fcc1b..fa8e07abc 100644 --- a/Kentor.AuthServices/SAML2P/ISaml2Message.cs +++ b/Kentor.AuthServices/SAML2P/ISaml2Message.cs @@ -48,6 +48,12 @@ public interface ISaml2Message /// X509Certificate2 SigningCertificate { get; } + /// + /// The signing algorithm to use when signing the message during binding, + /// according to the signature processing rules of each binding. + /// + /// The signing algorithm. + MessageSigningAlgorithm SigningAlgorithm { get; } /// /// Issuer of the message. /// diff --git a/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs b/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs index 3414288e3..eeb9d9a72 100644 --- a/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs +++ b/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs @@ -168,5 +168,6 @@ private void ValidateCorrectDocument(XmlElement xml) /// to the signature processing rules of each binding. /// public X509Certificate2 SigningCertificate { get; set; } + public MessageSigningAlgorithm SigningAlgorithm { get; set; } } } diff --git a/Kentor.AuthServices/SAML2P/Saml2Response.cs b/Kentor.AuthServices/SAML2P/Saml2Response.cs index 2e9c7a6a9..d7547f178 100644 --- a/Kentor.AuthServices/SAML2P/Saml2Response.cs +++ b/Kentor.AuthServices/SAML2P/Saml2Response.cs @@ -227,6 +227,8 @@ private void ReadAndValidateInResponseTo(XmlElement xml, Saml2Id expectedInRespo /// [ExcludeFromCodeCoverage] public X509Certificate2 SigningCertificate { get; } + [ExcludeFromCodeCoverage] + public MessageSigningAlgorithm SigningAlgorithm { get; set; } private XmlElement xmlElement; diff --git a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs index 59bd40d6f..998c0aa38 100644 --- a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs +++ b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs @@ -55,6 +55,8 @@ protected Saml2StatusResponseType(Saml2StatusCode status) /// public X509Certificate2 SigningCertificate { get; set; } + public MessageSigningAlgorithm SigningAlgorithm { get; set; } + /// /// Status code of the message. /// diff --git a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs index 10315fcc6..7bf1df84c 100644 --- a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs +++ b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs @@ -1,6 +1,7 @@ using Kentor.AuthServices.Configuration; using Kentor.AuthServices.Saml2P; using System; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; @@ -51,13 +52,13 @@ public override UnbindResult Unbind(HttpRequestData request, IOptions options) public override CommandResult Bind(ISaml2Message message) { - if(message == null) + if (message == null) { throw new ArgumentNullException(nameof(message)); } var xml = message.ToXml(); - if(message.SigningCertificate != null) + if (message.SigningCertificate != null) { var xmlDoc = new XmlDocument() { @@ -65,26 +66,24 @@ public override CommandResult Bind(ISaml2Message message) }; xmlDoc.LoadXml(xml); - xmlDoc.Sign(message.SigningCertificate, true); + + switch (message.SigningAlgorithm) + { + case MessageSigningAlgorithm.RsaSecureHashAlgorithm1: + xmlDoc.Sign(message.SigningCertificate, true); + break; + default: + xmlDoc.SignDocument(message.SigningCertificate, message.SigningAlgorithm); + break; + } xml = xmlDoc.OuterXml; } var encodedXml = Convert.ToBase64String(Encoding.UTF8.GetBytes(xml)); - var relayStateHtml = string.IsNullOrEmpty(message.RelayState) ? null - : string.Format(CultureInfo.InvariantCulture, PostHtmlRelayStateFormatString, message.RelayState); + var relayStateHtml = string.IsNullOrEmpty(message.RelayState) ? null : string.Format(CultureInfo.InvariantCulture, PostHtmlRelayStateFormatString, message.RelayState); - var cr = new CommandResult() - { - ContentType = "text/html", - Content = String.Format( - CultureInfo.InvariantCulture, - PostHtmlFormatString, - message.DestinationUrl, - relayStateHtml, - message.MessageName, - encodedXml) - }; + var cr = new CommandResult() {ContentType = "text/html", Content = String.Format(CultureInfo.InvariantCulture, PostHtmlFormatString, message.DestinationUrl, relayStateHtml, message.MessageName, encodedXml)}; return cr; } diff --git a/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs b/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs index e906f52ab..758d148bd 100644 --- a/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs +++ b/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs @@ -2,7 +2,6 @@ using Kentor.AuthServices.Exceptions; using Kentor.AuthServices.Saml2P; using System; -using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Metadata; using System.IdentityModel.Tokens; @@ -11,13 +10,8 @@ using System.Linq; using System.Net; using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography.Xml; using System.Text; -using System.Threading.Tasks; -using System.Web; using System.Xml; -using System.Xml.Linq; namespace Kentor.AuthServices.WebSso { @@ -38,7 +32,7 @@ public override CommandResult Bind(ISaml2Message message) if(message.SigningCertificate != null) { - queryString = AddSignature(queryString, message.SigningCertificate); + queryString = AddSignature(queryString, message); } var redirectUri = new Uri(message.DestinationUrl.ToString() @@ -52,24 +46,21 @@ public override CommandResult Bind(ISaml2Message message) }; } - private static string AddSignature(string queryString, X509Certificate2 key) + private static string AddSignature(string queryString, ISaml2Message message) { - queryString += "&SigAlg=" + Uri.EscapeDataString(SignedXml.XmlDsigRSASHA1Url); + string signingAlgorithmUrl = message.SigningAlgorithm.ToNamespace(); - var signatureDescription = - (SignatureDescription)CryptoConfig.CreateFromName(SignedXml.XmlDsigRSASHA1Url); - - var hashAlg = signatureDescription.CreateDigest(); + queryString += "&SigAlg=" + Uri.EscapeDataString(signingAlgorithmUrl); + var signatureDescription = (SignatureDescription)CryptoConfig.CreateFromName(signingAlgorithmUrl); + HashAlgorithm hashAlg = signatureDescription.CreateDigest(); hashAlg.ComputeHash(Encoding.UTF8.GetBytes(queryString)); - var asymmetricSignatureFormatter = signatureDescription.CreateFormatter(key.PrivateKey); - var signatureValue = asymmetricSignatureFormatter.CreateSignature(hashAlg); - + AsymmetricSignatureFormatter asymmetricSignatureFormatter = signatureDescription.CreateFormatter(message.SigningCertificate.PrivateKey); + byte[] signatureValue = asymmetricSignatureFormatter.CreateSignature(hashAlg); queryString += "&Signature=" + Uri.EscapeDataString(Convert.ToBase64String(signatureValue)); - return queryString; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification="The MemoryStream is not disposed by the DeflateStream - we're using the keep-open flag.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "The MemoryStream is not disposed by the DeflateStream - we're using the keep-open flag.")] public override UnbindResult Unbind(HttpRequestData request, IOptions options) { if (request == null) @@ -77,9 +68,7 @@ public override UnbindResult Unbind(HttpRequestData request, IOptions options) throw new ArgumentNullException(nameof(request)); } - var payload = Convert.FromBase64String( - request.QueryString["SAMLRequest"].FirstOrDefault() - ?? request.QueryString["SAMLResponse"].First()); + var payload = Convert.FromBase64String(request.QueryString["SAMLRequest"].FirstOrDefault() ?? request.QueryString["SAMLResponse"].First()); using (var compressed = new MemoryStream(payload)) { using (var decompressedStream = new DeflateStream(compressed, CompressionMode.Decompress, true)) @@ -88,51 +77,39 @@ public override UnbindResult Unbind(HttpRequestData request, IOptions options) { decompressedStream.CopyTo(deCompressed); - var xml = new XmlDocument() - { - PreserveWhitespace = true - }; + var xml = new XmlDocument() {PreserveWhitespace = true}; xml.LoadXml(Encoding.UTF8.GetString(deCompressed.GetBuffer())); - return new UnbindResult( - xml.DocumentElement, - request.QueryString["RelayState"].SingleOrDefault(), - GetTrustLevel(xml.DocumentElement, request, options)); + return new UnbindResult(xml.DocumentElement, request.QueryString["RelayState"].SingleOrDefault(), GetTrustLevel(xml.DocumentElement, request, options)); } } } } - private static TrustLevel GetTrustLevel( - XmlElement documentElement, - HttpRequestData request, - IOptions options) + private static TrustLevel GetTrustLevel(XmlElement documentElement, HttpRequestData request, IOptions options) { - if(options == null) + if (options == null) { return TrustLevel.None; } - if(!request.QueryString["SigAlg"].Any()) + if (!request.QueryString["SigAlg"].Any()) { return TrustLevel.None; } var issuer = documentElement["Issuer", Saml2Namespaces.Saml2Name]?.InnerText; - - if(string.IsNullOrEmpty(issuer)) + + if (string.IsNullOrEmpty(issuer)) { return TrustLevel.None; } IdentityProvider idp; - if(!options.IdentityProviders.TryGetValue(new EntityId(issuer), out idp)) + if (!options.IdentityProviders.TryGetValue(new EntityId(issuer), out idp)) { - throw new InvalidSignatureException( - string.Format(CultureInfo.InvariantCulture, - "Cannot verify signature of message from unknown sender {0}.", - issuer)); + throw new InvalidSignatureException(string.Format(CultureInfo.InvariantCulture, "Cannot verify signature of message from unknown sender {0}.", issuer)); } CheckSignature(request, idp); @@ -145,14 +122,11 @@ private static void CheckSignature(HttpRequestData request, IdentityProvider idp // Can't use the query string params as found in HttpReqeustData // because they are already unescaped and we need the exact format // of the original data. - var rawQueryStringParams = request.Url.Query.TrimStart('?') - .Split('&') - .Select(qp => qp.Split('=')) - .ToDictionary(kv => kv[0], kv => kv[1]); + var rawQueryStringParams = request.Url.Query.TrimStart('?').Split('&').Select(qp => qp.Split('=')).ToDictionary(kv => kv[0], kv => kv[1]); var msgParam = ""; string msg; - if(rawQueryStringParams.TryGetValue("SAMLRequest", out msg)) + if (rawQueryStringParams.TryGetValue("SAMLRequest", out msg)) { msgParam = "SAMLRequest=" + msg; } @@ -168,33 +142,24 @@ private static void CheckSignature(HttpRequestData request, IdentityProvider idp relayStateParam = "&RelayState=" + relayState; } - var signedString = string.Format(CultureInfo.InvariantCulture, - "{0}{1}&SigAlg={2}", - msgParam, - relayStateParam, - rawQueryStringParams["SigAlg"]); + var signedString = string.Format(CultureInfo.InvariantCulture, "{0}{1}&SigAlg={2}", msgParam, relayStateParam, rawQueryStringParams["SigAlg"]); var sigAlg = request.QueryString["SigAlg"].Single(); - var signatureDescription = (SignatureDescription)CryptoConfig.CreateFromName(sigAlg); + var signatureDescription = (SignatureDescription) CryptoConfig.CreateFromName(sigAlg); var hashAlg = signatureDescription.CreateDigest(); hashAlg.ComputeHash(Encoding.UTF8.GetBytes(signedString)); var signature = Convert.FromBase64String(request.QueryString["Signature"].Single()); - if(!idp.SigningKeys.Any( - kic => signatureDescription.CreateDeformatter( - ((AsymmetricSecurityKey)kic.CreateKey()).GetAsymmetricAlgorithm(sigAlg, false)) - .VerifySignature(hashAlg, signature))) + if (!idp.SigningKeys.Any(kic => signatureDescription.CreateDeformatter(((AsymmetricSecurityKey) kic.CreateKey()).GetAsymmetricAlgorithm(sigAlg, false)).VerifySignature(hashAlg, signature))) { - throw new InvalidSignatureException(string.Format(CultureInfo.InvariantCulture, - "Message from {0} failed signature verification", - idp.EntityId.Id)); + throw new InvalidSignatureException(string.Format(CultureInfo.InvariantCulture, "Message from {0} failed signature verification", idp.EntityId.Id)); } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification="The MemoryStream is not disposed by the DeflateStream - we're using the keep-open flag.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "The MemoryStream is not disposed by the DeflateStream - we're using the keep-open flag.")] private static string Serialize(string payload) { using (var compressed = new MemoryStream()) @@ -210,13 +175,12 @@ private static string Serialize(string payload) protected internal override bool CanUnbind(HttpRequestData request) { - if(request == null) + if (request == null) { throw new ArgumentNullException(nameof(request)); } - return (request.QueryString["SAMLRequest"].Any() - || request.QueryString["SAMLResponse"].Any()); + return (request.QueryString["SAMLRequest"].Any() || request.QueryString["SAMLResponse"].Any()); } } } diff --git a/Kentor.AuthServices/XmlDocumentSigningExtensions.cs b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs new file mode 100644 index 000000000..70bf57eb4 --- /dev/null +++ b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Xml; + +using Kentor.AuthServices.Saml2P; + +namespace Kentor.AuthServices +{ + /// + /// Extension methods for XmlDocument + /// + public static class XmlDocumentSigningExtensions + { + private static readonly Dictionary _algorithmToNamespaceMap = new Dictionary + { + { MessageSigningAlgorithm.RsaSecureHashAlgorithm1,RSASHA1}, + { MessageSigningAlgorithm.RsaSecureHashAlgorithm256,RSASHA256}, + { MessageSigningAlgorithm.RsaSecureHashAlgorithm384,RSASHA384}, + { MessageSigningAlgorithm.RsaSecureHashAlgorithm512,RSASHA512} + }; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "RSASHA")] + const string RSASHA1 = SignedXml.XmlDsigRSASHA1Url;//"http://www.w3.org/2000/09/xmldsig#rsa-sha1"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "RSASHA")] + const string RSASHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "RSASHA")] + const string RSASHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "RSASHA")] + const string RSASHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"; + + /// + /// Resolve w3 org namespace from algorithm enumeration + /// + /// + /// + public static string ToNamespace(this MessageSigningAlgorithm algorithm) + { + return _algorithmToNamespaceMap[algorithm]; + } + + /// + /// Add digital signature to an xml document. + /// The default signing algorithm RSA SHA-256 is used in this method. + /// + /// + /// + public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate) + { + SignDocument(xmlDocument, signingCertificate, MessageSigningAlgorithm.RsaSecureHashAlgorithm256); + } + + /// + /// Add digital signature to an xml document. + /// + /// The XML document. + /// The signing certificate. + /// The signing algorithm. + /// + /// + public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, MessageSigningAlgorithm algorithm) + { + if (xmlDocument == null) + { + throw new ArgumentNullException(nameof(xmlDocument)); + } + if (xmlDocument.DocumentElement == null) + { + throw new ArgumentNullException(nameof(xmlDocument), "The property DocumentElement cannot be null"); + } + + if (signingCertificate == null) + { + throw new ArgumentNullException(nameof(signingCertificate)); + } + + string signatureMethodNamespace = algorithm.ToNamespace(); + + // Note that this will return a Basic crypto provider, with only SHA-1 support + var key = (RSACryptoServiceProvider) signingCertificate.PrivateKey; + + using (var provider = new RSACryptoServiceProvider()) + { + CspKeyContainerInfo enhCsp = provider.CspKeyContainerInfo; + using (key = new RSACryptoServiceProvider(new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, key.CspKeyContainerInfo.KeyContainerName))) + { + var signedXml = new SignedXml(xmlDocument); + signedXml.SigningKey = key; + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + signedXml.SignedInfo.SignatureMethod = signatureMethodNamespace; + + var reference = new Reference {Uri = "#" + xmlDocument.DocumentElement.GetAttribute("ID")}; + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(reference); + signedXml.ComputeSignature(); + xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signedXml.GetXml(), true)); + } + } + } + } +} \ No newline at end of file From 77a88469d6fa6252ad1ac6845c3d95a9cf4e2334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20T=C3=B8nners?= Date: Wed, 9 Nov 2016 17:29:07 +0100 Subject: [PATCH 2/6] Coverage fix and support for configuration of authentication request signing algorithm per identityprovider. --- Kentor.AuthServices.Tests/App.config | 16 +- ...thServicesAuthenticationMiddlewareTests.cs | 37 + .../WebSSO/Saml2MessageImplementation.cs | 1 + Kentor.AuthServices.Tests/XmlHelpersTests.cs | 778 +++++++++--------- .../Configuration/IdentityProviderElement.cs | 35 + .../KentorAuthServicesSection.cs | 14 +- .../Configuration/SPOptions.cs | 7 +- Kentor.AuthServices/IdentityProvider.cs | 10 +- .../MessageSigningAlgorithm.cs | 12 + .../SAML2P/Saml2StatusResponseType.cs | 2 +- .../WebSSO/Saml2PostBinding.cs | 2 - .../XmlDocumentSigningExtensions.cs | 31 +- 12 files changed, 550 insertions(+), 395 deletions(-) diff --git a/Kentor.AuthServices.Tests/App.config b/Kentor.AuthServices.Tests/App.config index ecc90053b..25af60dfe 100644 --- a/Kentor.AuthServices.Tests/App.config +++ b/Kentor.AuthServices.Tests/App.config @@ -5,7 +5,9 @@
- + @@ -41,6 +43,18 @@ + + + + + + diff --git a/Kentor.AuthServices.Tests/Owin/KentorAuthServicesAuthenticationMiddlewareTests.cs b/Kentor.AuthServices.Tests/Owin/KentorAuthServicesAuthenticationMiddlewareTests.cs index de04bd960..85162288f 100644 --- a/Kentor.AuthServices.Tests/Owin/KentorAuthServicesAuthenticationMiddlewareTests.cs +++ b/Kentor.AuthServices.Tests/Owin/KentorAuthServicesAuthenticationMiddlewareTests.cs @@ -204,6 +204,43 @@ public async Task KentorAuthServicesAuthenticationMiddleware_CreatesPostOnAuthCh } } + + [TestMethod] + public async Task KentorAuthServicesAuthenticationMiddleware_CreatesSignedPostOnAuthChallenge() + { + var middleware = new KentorAuthServicesAuthenticationMiddleware( + new StubOwinMiddleware(401, new AuthenticationResponseChallenge( + new string[] { "KentorAuthServices" }, new AuthenticationProperties( + new Dictionary() + { + { "idp", "https://idp4.example.com" } + }))), + CreateAppBuilder(), + new KentorAuthServicesAuthenticationOptions(true) + ); + + var context = OwinTestHelpers.CreateOwinContext(); + + await middleware.Invoke(context); + + context.Response.StatusCode.Should().Be(200); + context.Response.Body.Seek(0, SeekOrigin.Begin); + + // Fix to #295, where content length is incorrectly set to 0 by the + // next middleware. It appears as it works if the content length is + // simply removed. See discussion in GitHub issue #295. + context.Response.ContentLength.Should().NotHaveValue(); + + using (var reader = new StreamReader(context.Response.Body)) + { + string bodyContent = reader.ReadToEnd(); + + // Checking some random stuff in body to make sure it looks like a SAML Post. + bodyContent.Should().Contain("
xd.Sign(TestCert); - - a.ShouldThrow().And.ParamName.Should().Be("xmlDocument"); - } - - [TestMethod] - public void XmlHelpers_Sign_Nullcheck_xmlElement() - { - ((XmlElement)null).Invoking( - x => x.Sign(SignedXmlHelper.TestCert, true)) - .ShouldThrow() - .And.ParamName.Should().Be("xmlElement"); - } - - [TestMethod] - public void XmlHelpers_Sign_Nullcheck_Cert() - { - xmlDocument.DocumentElement.Invoking( - x => x.Sign(null, false)) - .ShouldThrow() - .And.ParamName.Should().Be("cert"); - } - - [TestMethod] - public void XmlHelpers_Sign() - { - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml("Some Content"); - - xmlDoc.Sign(TestCert); - - var signature = xmlDoc.DocumentElement["Signature", SignedXml.XmlDsigNamespaceUrl]; - - signature["SignedInfo", SignedXml.XmlDsigNamespaceUrl] - ["Reference", SignedXml.XmlDsigNamespaceUrl].Attributes["URI"].Value - .Should().Be("#rootElementId"); - - var signedXml = new SignedXml(xmlDoc); - signedXml.LoadXml(signature); - signedXml.CheckSignature(TestCert, true).Should().BeTrue(); - } - - const string xmlString = "\n content\n"; - readonly XmlDocument xmlDocument = XmlHelpers.FromString(xmlString); - - [TestMethod] - public void XmlHelpers_FromString() - { - xmlDocument.OuterXml.Should().Be(xmlString); - } - - [TestMethod] - public void XmlHelpers_Remove_NullcheckAttribute() - { - ((XmlAttributeCollection)null).Invoking( - a => a.Remove("attributeName")) - .ShouldThrow() - .And.ParamName.Should().Be("attributes"); - } - - [TestMethod] - public void XmlHelpers_Remove_NullcheckAttributeName() - { - xmlDocument.DocumentElement.Attributes.Invoking( - a => a.Remove(attributeName: null)) - .ShouldThrow() - .And.ParamName.Should().Be("attributeName"); - } - - [TestMethod] - public void XmlHelpers_RemoveChild_NullcheckXmlElement() - { - new XmlDocument().DocumentElement.Invoking( - e => e.RemoveChild("name", "ns")) - .ShouldThrow() - .And.ParamName.Should().Be("xmlElement"); - } - - [TestMethod] - public void XmlHelpers_RemoveChild_NullcheckName() - { - xmlDocument.DocumentElement.Invoking( - e => e.RemoveChild(null, "ns")) - .ShouldThrow() - .And.ParamName.Should().Be("name"); - } - - [TestMethod] - public void XmlHelpers_RemoveChild_NullcheckNs() - { - xmlDocument.DocumentElement.Invoking( - e => e.RemoveChild("name", null)) - .ShouldThrow() - .And.ParamName.Should().Be("ns"); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_NullcheckXmlElement() - { - ((XmlElement)null).Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow("xmlElement"); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_NullcheckCertificate() - { - xmlDocument.DocumentElement.Invoking( - x => x.IsSignedBy(null)) - .ShouldThrow("certificate"); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy() - { - var xml = "text"; - var xmlDoc = XmlHelpers.FromString(xml); - xmlDoc.Sign(SignedXmlHelper.TestCert); - - xmlDoc.DocumentElement.IsSignedBy(SignedXmlHelper.TestCert).Should().BeTrue(); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsOnWrongCert() - { - var xml = "text"; - var xmlDoc = XmlHelpers.FromString(xml); - xmlDoc.Sign(SignedXmlHelper.TestCert2, true); - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .And.Message.Should().Be("The signature verified correctly with the key contained in the signature, but that key is not trusted."); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsOnTamperedData() - { - var xml = "text"; - var xmlDoc = XmlHelpers.FromString(xml); - xmlDoc.Sign(SignedXmlHelper.TestCert); - - xmlDoc.DocumentElement["content"].InnerText = "changedText"; - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .And.Message.Should().Be("Signature didn't verify. Have the contents been tampered with?"); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_TrowsOnSignatureWrapping() - { - var xml = "text" - + "other text"; - var xmlDoc = XmlHelpers.FromString(xml); - - xmlDoc.DocumentElement["content"].Sign(SignedXmlHelper.TestCert, false); - - // An XML wrapping attack is created by taking a legitimate signature - // and putting it in another element. If the reference of the signature - // is not properly checked, the element containing the signature - // is incorrectly trusted. - var signatureNode = xmlDoc.DocumentElement["content"]["Signature", SignedXml.XmlDsigNamespaceUrl]; - xmlDoc.DocumentElement["content"].RemoveChild(signatureNode); - xmlDoc.DocumentElement["injected"].AppendChild(signatureNode); - - xmlDoc.DocumentElement["injected"].Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .And.Message.Should().Be("Incorrect reference on Xml signature. The reference must be to the root element of the element containing the signature."); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_FalseOnMissingSignature() - { - var xml = "text"; - var xmlDoc = XmlHelpers.FromString(xml); - - xmlDoc.DocumentElement.IsSignedBy(SignedXmlHelper.TestCert).Should().BeFalse(); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsOnMissingReferenceInSignature() - { - var signedWithoutReference = @"https://idp.example.comtYFIoYmrzmp3H7TXm9IS8DW3buBZIb6sI2ycrn+AOnVcdYnPTJpk3ntHlqQKXNEyXgXZNdqEuFpgI1I0P0TlhM+C3rBJnflkApkxZkak5RwnJzDWTHpsSDjYcm+/XgBy3JVZJuMWb2YPaV8GB6cjBMDrENUEaoKRg+FpzPUZO1EOMcqbocXp5cHie1CkPnD1OtT/cuzMBUMpBGZMxjZwdFpOO7R3CUXh/McxKfoGUQGC3DVpt5T8uGkpj4KqZVPS/qTCRhbPRDjg73BdWbdkFpFWge8G/FgkYxr9LBE1TsrxptppO9xoA5jXwJVZaWndSMvo6TuOjUgqY2w5RTkqhA=="; - - var xmlDoc = XmlHelpers.FromString(signedWithoutReference); - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .And.Message.Should().Be("No reference found in Xml signature, it doesn't validate the Xml data."); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsOnDualReferencesInSignature() - { - var xml = ""; - - var xmlDoc = XmlHelpers.FromString(xml); - - var signedXml = new SignedXml(xmlDoc); - signedXml.SigningKey = (RSACryptoServiceProvider)SignedXmlHelper.TestCert.PrivateKey; - signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; - - var ref1 = new Reference { Uri = "#myxml" }; - ref1.AddTransform(new XmlDsigEnvelopedSignatureTransform()); - ref1.AddTransform(new XmlDsigExcC14NTransform()); - signedXml.AddReference(ref1); - - var ref2 = new Reference { Uri = "#myxml" }; - ref2.AddTransform(new XmlDsigEnvelopedSignatureTransform()); - ref2.AddTransform(new XmlDsigExcC14NTransform()); - signedXml.AddReference(ref2); - - signedXml.ComputeSignature(); - xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(signedXml.GetXml(), true)); - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .And.Message.Should().Be("Multiple references for Xml signatures are not allowed."); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsInformativeMessageOnSha256Signature() - { - // With .Net 4.6.2 and above this test will not throw any error because the SHA256 is now built-in - if ( CryptoConfig.CreateFromName( "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" ) != null ) return; - - var xmlSignedWithSha256 = @"https://idp.example.comF+E7u3vqMC07ipvP9AowsMqP7y6CsAC0GeEIxNSwDEI=GmiXn24Ccnr64TbmDd1/nLM+891z0FtRHSpU8+75uOqbpNK/ZZGrltFf2YZ5u9b9O0HfbFFsZ0i28ocwAZOv2UfxQrCtOGf3ss7Q+t2Zmc6Q/3ES7HIa15I5BbaSdNfpOMlX6N1XXhMprRGy2YWMr5IAIhysFG1A2oHaC3yFiesfUrawN/lXUYuI22Kf4A5bmnIkKijnwX9ewnhRj6569bw+c6q+tVZSHQzI+KMU9KbKN4NsXxAmv6dM1w2qOiX9/CO9LzwEtlhA9yo3sl0uWP8z5GwK9qgOlsF2NdImAQ5f0U4Uv26doFn09W+VExFwNhcXhewQUuPBYBr+XXzdww==MIIDIzCCAg+gAwIBAgIQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAMCQxIjAgBgNVBAMTGUtlbnRvci5BdXRoU2VydmljZXMuVGVzdHMwHhcNMTMwOTI1MTMzNTQ0WhcNMzkxMjMxMjM1OTU5WjAkMSIwIAYDVQQDExlLZW50b3IuQXV0aFNlcnZpY2VzLlRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVGpfvK9N//MnA5Jo1q2liyPR24406Dp25gv7LB3HK4DWgqsb7xXM6KIV/WVOyCV2g/O1ErBlB+HLhVZ4XUJvbqBbgAJqFO+TZwcCIe8u4nTEXeU660FdtkKClA17sbtMrAGdDfOPwVBHSuavdHeD7jHNI4RUDGKnEW13/0EvnHDilIetwODRxrX/+41R24sJThFbMczByS3OAL2dcIxoAynaGeM90gXsVYow1QhJUy21+cictikb7jW4mW6dvFCBrWIceom9J295DcQIHoxJy5NoZwMir/JV00qs1wDVoN20Ve1DC5ImwcG46XPF7efQ44yLh2j5Yexw+xloA81dwIDAQABo1kwVzBVBgNVHQEETjBMgBAWIahoZhXVUogbAqkS7zwfoSYwJDEiMCAGA1UEAxMZS2VudG9yLkF1dGhTZXJ2aWNlcy5UZXN0c4IQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAA4IBAQA2aGzmuKw4AYXWMhrGj5+i8vyAoifUn1QVOFsUukEA77CrqhqqaWFoeagfJp/45vlvrfrEwtF0QcWfmO9w1VvHwm7sk1G/cdYyJ71sU+llDsdPZm7LxQvWZYkK+xELcinQpSwt4ExavS+jLcHoOYHYwIZMBn3U8wZw7Kq29oGnoFQz7HLCEl/G9i3QRyvFITNlWTjoScaqMjHTzq6HCMaRsL09DLcY3KB+cedfpC0/MBlzaxZv0DctTulyaDfM9DCYOyokGN/rQ6qkAR0DDm8fVwknbJY7kURXNGoUetulTb5ow8BvD1gncOaYHSD0kbHZG+bLsUZDFatEr2KW8jbGSomeUser"; - - var xmlDoc = XmlHelpers.FromString(xmlSignedWithSha256); - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCertSignOnly)) - .ShouldThrow() - .And.Message.Should().Be("SHA256 signatures require the algorithm to be registered at the process level. Upgrade to .Net 4.6.2 or call Kentor.AuthServices.Configuration.Options.GlobalEnableSha256XmlSignatures() on startup to register."); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsOnTamperedData_WithSha256Signature() - { - Options.GlobalEnableSha256XmlSignatures(); - - var xml = @"https://idp.example.comBKRyWqweAczLA8fgRcx6zzMDiP0qT0TwqU/X4VgLiXM=iK8s+MkLlixSSQu5Q/SHRZLhfnj4jlyPLAD6C2n9zmQu4CosZME7mxiNFiWyOE8XRGd+2LJle+NjJrkZFktVb03JaToq7w4Q8GfJ2oUUjNCweoaJ6NzsnwkFoXhyh0dfOixl/Ifa3qDX50/Hv2twF/QXfDs08GZTxZKehKsVDITyVd6nytF8VUb0+nU7UMWPn1XeHM7YNI/1mkVbCRx/ci5ZRxwjAX40xttd4JL6oBnp5oaaMgWpAa2cVb+t/9HhCRThEho1etbPHx/+E9ElL1PhKqKX6nh2GSH1TFJkwEXIPPZKqCs3YDINLBZpLfl626zbV4cGOGyWUAroVsk2uw==MIIDIzCCAg+gAwIBAgIQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAMCQxIjAgBgNVBAMTGUtlbnRvci5BdXRoU2VydmljZXMuVGVzdHMwHhcNMTMwOTI1MTMzNTQ0WhcNMzkxMjMxMjM1OTU5WjAkMSIwIAYDVQQDExlLZW50b3IuQXV0aFNlcnZpY2VzLlRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVGpfvK9N//MnA5Jo1q2liyPR24406Dp25gv7LB3HK4DWgqsb7xXM6KIV/WVOyCV2g/O1ErBlB+HLhVZ4XUJvbqBbgAJqFO+TZwcCIe8u4nTEXeU660FdtkKClA17sbtMrAGdDfOPwVBHSuavdHeD7jHNI4RUDGKnEW13/0EvnHDilIetwODRxrX/+41R24sJThFbMczByS3OAL2dcIxoAynaGeM90gXsVYow1QhJUy21+cictikb7jW4mW6dvFCBrWIceom9J295DcQIHoxJy5NoZwMir/JV00qs1wDVoN20Ve1DC5ImwcG46XPF7efQ44yLh2j5Yexw+xloA81dwIDAQABo1kwVzBVBgNVHQEETjBMgBAWIahoZhXVUogbAqkS7zwfoSYwJDEiMCAGA1UEAxMZS2VudG9yLkF1dGhTZXJ2aWNlcy5UZXN0c4IQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAA4IBAQA2aGzmuKw4AYXWMhrGj5+i8vyAoifUn1QVOFsUukEA77CrqhqqaWFoeagfJp/45vlvrfrEwtF0QcWfmO9w1VvHwm7sk1G/cdYyJ71sU+llDsdPZm7LxQvWZYkK+xELcinQpSwt4ExavS+jLcHoOYHYwIZMBn3U8wZw7Kq29oGnoFQz7HLCEl/G9i3QRyvFITNlWTjoScaqMjHTzq6HCMaRsL09DLcY3KB+cedfpC0/MBlzaxZv0DctTulyaDfM9DCYOyokGN/rQ6qkAR0DDm8fVwknbJY7kURXNGoUetulTb5ow8BvD1gncOaYHSD0kbHZG+bLsUZDFatEr2KW8jbGSomeUser"; - xml = xml.Replace("SomeUser", "OtherUser"); - - var xmlDoc = XmlHelpers.FromString(xml); - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .WithMessage("Signature didn't verify. Have the contents been tampered with?"); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_ThrowsOnIncorrectTransformsInSignature() - { - // SAML2 Core 5.4.4 states that signatures SHOULD NOT contain other transforms than - // the enveloped signature or exclusive canonicalization transforms and that a verifier - // of a signature MAY reject signatures with other transforms. We'll reject them to - // mitigate the risk of transforms opening up for assertion injections. - - var xml = ""; - - var xmlDoc = XmlHelpers.FromString(xml); - - var signedXml = new SignedXml(xmlDoc); - signedXml.SigningKey = (RSACryptoServiceProvider)SignedXmlHelper.TestCert.PrivateKey; - signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; - - var reference = new Reference { Uri = "#MyXml" }; - reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); - reference.AddTransform(new XmlDsigC14NTransform()); // The allowed transform is XmlDsigExcC14NTransform - signedXml.AddReference(reference); - - signedXml.ComputeSignature(); - xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(signedXml.GetXml(), true)); - - xmlDoc.DocumentElement.Invoking( - x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .And.Message.Should().Be("Transform \"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\" found in Xml signature SHOULD NOT be used with SAML2."); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_HandlesMultipleSignaturesInSameDocument() - { - var content1 = SignedXmlHelper.SignXml(""); - var content2 = SignedXmlHelper.SignXml(""); - - var xml = -$@" - {content1} - {content2} -"; - - var xmlDoc = XmlHelpers.FromString(xml); - - ((XmlElement)xmlDoc.SelectSingleNode("//*[@ID=\"c1\"]")).IsSignedBy(SignedXmlHelper.TestCert) - .Should().BeTrue("first content is correclty signed"); - - ((XmlElement)xmlDoc.SelectSingleNode("//*[@ID=\"c2\"]")).IsSignedBy(SignedXmlHelper.TestCert) - .Should().BeTrue("second content is correclty signed"); - } - - [TestMethod] - public void XmlHelpers_IsSignedBy_DoesNotThrowSha256MessageForOtherProblem() - { - Options.GlobalEnableSha256XmlSignatures(); - - // Here I've specified an invalid digest algorithm. Want to be sure it - // does NOT flag this as a problem with the sha256 signature algorithm - // (they both throw CryptographicException) - var xml = - @"https://idp.example.comF+E7u3vqMC07ipvP9AowsMqP7y6CsAC0GeEIxNSwDEI=GmiXn24Ccnr64TbmDd1/nLM+891z0FtRHSpU8+75uOqbpNK/ZZGrltFf2YZ5u9b9O0HfbFFsZ0i28ocwAZOv2UfxQrCtOGf3ss7Q+t2Zmc6Q/3ES7HIa15I5BbaSdNfpOMlX6N1XXhMprRGy2YWMr5IAIhysFG1A2oHaC3yFiesfUrawN/lXUYuI22Kf4A5bmnIkKijnwX9ewnhRj6569bw+c6q+tVZSHQzI+KMU9KbKN4NsXxAmv6dM1w2qOiX9/CO9LzwEtlhA9yo3sl0uWP8z5GwK9qgOlsF2NdImAQ5f0U4Uv26doFn09W+VExFwNhcXhewQUuPBYBr+XXzdww==MIIDIzCCAg+gAwIBAgIQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAMCQxIjAgBgNVBAMTGUtlbnRvci5BdXRoU2VydmljZXMuVGVzdHMwHhcNMTMwOTI1MTMzNTQ0WhcNMzkxMjMxMjM1OTU5WjAkMSIwIAYDVQQDExlLZW50b3IuQXV0aFNlcnZpY2VzLlRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVGpfvK9N//MnA5Jo1q2liyPR24406Dp25gv7LB3HK4DWgqsb7xXM6KIV/WVOyCV2g/O1ErBlB+HLhVZ4XUJvbqBbgAJqFO+TZwcCIe8u4nTEXeU660FdtkKClA17sbtMrAGdDfOPwVBHSuavdHeD7jHNI4RUDGKnEW13/0EvnHDilIetwODRxrX/+41R24sJThFbMczByS3OAL2dcIxoAynaGeM90gXsVYow1QhJUy21+cictikb7jW4mW6dvFCBrWIceom9J295DcQIHoxJy5NoZwMir/JV00qs1wDVoN20Ve1DC5ImwcG46XPF7efQ44yLh2j5Yexw+xloA81dwIDAQABo1kwVzBVBgNVHQEETjBMgBAWIahoZhXVUogbAqkS7zwfoSYwJDEiMCAGA1UEAxMZS2VudG9yLkF1dGhTZXJ2aWNlcy5UZXN0c4IQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAA4IBAQA2aGzmuKw4AYXWMhrGj5+i8vyAoifUn1QVOFsUukEA77CrqhqqaWFoeagfJp/45vlvrfrEwtF0QcWfmO9w1VvHwm7sk1G/cdYyJ71sU+llDsdPZm7LxQvWZYkK+xELcinQpSwt4ExavS+jLcHoOYHYwIZMBn3U8wZw7Kq29oGnoFQz7HLCEl/G9i3QRyvFITNlWTjoScaqMjHTzq6HCMaRsL09DLcY3KB+cedfpC0/MBlzaxZv0DctTulyaDfM9DCYOyokGN/rQ6qkAR0DDm8fVwknbJY7kURXNGoUetulTb5ow8BvD1gncOaYHSD0kbHZG+bLsUZDFatEr2KW8jbGSomeUser"; - - var xmlDoc = XmlHelpers.FromString(xml); - - xmlDoc.DocumentElement.Invoking(x => x.IsSignedBy(SignedXmlHelper.TestCert)) - .ShouldThrow() - .WithMessage("SignatureDescription could not be created for the signature algorithm supplied."); - } - - [TestMethod] - public void XmlHelpers_IsSignedByAny_ThrowsOnCertValidationWithRsaKey() - { - var xml = ""; - - var xmlDoc = XmlHelpers.FromString(xml); - xmlDoc.Sign(SignedXmlHelper.TestCert, false); - - var signingKeys = Enumerable.Repeat(SignedXmlHelper.TestKey, 1); - - xmlDoc.DocumentElement.Invoking(x => x.IsSignedByAny(signingKeys, true)) - .ShouldThrow() - .And.Message.Should().Be("Certificate validation enabled, but the signing key identifier is of type RsaKeyIdentifierClause which cannot be validated as a certificate."); - } - } +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Xml; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using FluentAssertions; +using Kentor.AuthServices.Tests.Helpers; +using Kentor.AuthServices.Exceptions; +using System.Security.Cryptography; +using System.Reflection; +using Kentor.AuthServices.Configuration; +using System.Collections.Generic; +using System.Linq; +using Kentor.AuthServices; +using Kentor.AuthServices.Saml2P; + +namespace Kentor.AuthServices.Tests +{ + [TestClass] + public class XmlHelpersTests + { + [TestCleanup] + public void Cleanup() + { + SignedXmlHelper.RemoveGlobalSha256XmlSignatureSupport(); + } + + public static readonly X509Certificate2 TestCert = new X509Certificate2("Kentor.AuthServices.Tests.pfx"); + + [TestMethod] + public void XmlHelpers_Sign_Nullcheck_xmlDocument() + { + XmlDocument xd = null; + Action a = () => xd.Sign(TestCert); + + a.ShouldThrow().And.ParamName.Should().Be("xmlDocument"); + } + + [TestMethod] + public void XmlHelpers_Sign_Nullcheck_xmlElement() + { + ((XmlElement)null).Invoking( + x => x.Sign(SignedXmlHelper.TestCert, true)) + .ShouldThrow() + .And.ParamName.Should().Be("xmlElement"); + } + + [TestMethod] + public void XmlHelpers_Sign_Nullcheck_Cert() + { + xmlDocument.DocumentElement.Invoking( + x => x.Sign(null, false)) + .ShouldThrow() + .And.ParamName.Should().Be("cert"); + } + + [TestMethod] + public void XmlHelpers_Sign() + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml("Some Content"); + + xmlDoc.Sign(TestCert); + + var signature = xmlDoc.DocumentElement["Signature", SignedXml.XmlDsigNamespaceUrl]; + + signature["SignedInfo", SignedXml.XmlDsigNamespaceUrl] + ["Reference", SignedXml.XmlDsigNamespaceUrl].Attributes["URI"].Value + .Should().Be("#rootElementId"); + + var signedXml = new SignedXml(xmlDoc); + signedXml.LoadXml(signature); + signedXml.CheckSignature(TestCert, true).Should().BeTrue(); + } + + const string xmlString = "\n content\n"; + readonly XmlDocument xmlDocument = XmlHelpers.FromString(xmlString); + + [TestMethod] + public void XmlHelpers_FromString() + { + xmlDocument.OuterXml.Should().Be(xmlString); + } + + [TestMethod] + public void XmlHelpers_Remove_NullcheckAttribute() + { + ((XmlAttributeCollection)null).Invoking( + a => a.Remove("attributeName")) + .ShouldThrow() + .And.ParamName.Should().Be("attributes"); + } + + [TestMethod] + public void XmlHelpers_Remove_NullcheckAttributeName() + { + xmlDocument.DocumentElement.Attributes.Invoking( + a => a.Remove(attributeName: null)) + .ShouldThrow() + .And.ParamName.Should().Be("attributeName"); + } + + [TestMethod] + public void XmlHelpers_RemoveChild_NullcheckXmlElement() + { + new XmlDocument().DocumentElement.Invoking( + e => e.RemoveChild("name", "ns")) + .ShouldThrow() + .And.ParamName.Should().Be("xmlElement"); + } + + [TestMethod] + public void XmlHelpers_RemoveChild_NullcheckName() + { + xmlDocument.DocumentElement.Invoking( + e => e.RemoveChild(null, "ns")) + .ShouldThrow() + .And.ParamName.Should().Be("name"); + } + + [TestMethod] + public void XmlHelpers_RemoveChild_NullcheckNs() + { + xmlDocument.DocumentElement.Invoking( + e => e.RemoveChild("name", null)) + .ShouldThrow() + .And.ParamName.Should().Be("ns"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_NullcheckXmlElement() + { + ((XmlElement)null).Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow("xmlElement"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_NullcheckCertificate() + { + xmlDocument.DocumentElement.Invoking( + x => x.IsSignedBy(null)) + .ShouldThrow("certificate"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy() + { + var xml = "text"; + var xmlDoc = XmlHelpers.FromString(xml); + xmlDoc.Sign(SignedXmlHelper.TestCert); + + xmlDoc.DocumentElement.IsSignedBy(SignedXmlHelper.TestCert).Should().BeTrue(); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsOnWrongCert() + { + var xml = "text"; + var xmlDoc = XmlHelpers.FromString(xml); + xmlDoc.Sign(SignedXmlHelper.TestCert2, true); + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .And.Message.Should().Be("The signature verified correctly with the key contained in the signature, but that key is not trusted."); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsOnTamperedData() + { + var xml = "text"; + var xmlDoc = XmlHelpers.FromString(xml); + xmlDoc.Sign(SignedXmlHelper.TestCert); + + xmlDoc.DocumentElement["content"].InnerText = "changedText"; + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .And.Message.Should().Be("Signature didn't verify. Have the contents been tampered with?"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_TrowsOnSignatureWrapping() + { + var xml = "text" + + "other text"; + var xmlDoc = XmlHelpers.FromString(xml); + + xmlDoc.DocumentElement["content"].Sign(SignedXmlHelper.TestCert, false); + + // An XML wrapping attack is created by taking a legitimate signature + // and putting it in another element. If the reference of the signature + // is not properly checked, the element containing the signature + // is incorrectly trusted. + var signatureNode = xmlDoc.DocumentElement["content"]["Signature", SignedXml.XmlDsigNamespaceUrl]; + xmlDoc.DocumentElement["content"].RemoveChild(signatureNode); + xmlDoc.DocumentElement["injected"].AppendChild(signatureNode); + + xmlDoc.DocumentElement["injected"].Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .And.Message.Should().Be("Incorrect reference on Xml signature. The reference must be to the root element of the element containing the signature."); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_FalseOnMissingSignature() + { + var xml = "text"; + var xmlDoc = XmlHelpers.FromString(xml); + + xmlDoc.DocumentElement.IsSignedBy(SignedXmlHelper.TestCert).Should().BeFalse(); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsOnMissingReferenceInSignature() + { + var signedWithoutReference = @"https://idp.example.comtYFIoYmrzmp3H7TXm9IS8DW3buBZIb6sI2ycrn+AOnVcdYnPTJpk3ntHlqQKXNEyXgXZNdqEuFpgI1I0P0TlhM+C3rBJnflkApkxZkak5RwnJzDWTHpsSDjYcm+/XgBy3JVZJuMWb2YPaV8GB6cjBMDrENUEaoKRg+FpzPUZO1EOMcqbocXp5cHie1CkPnD1OtT/cuzMBUMpBGZMxjZwdFpOO7R3CUXh/McxKfoGUQGC3DVpt5T8uGkpj4KqZVPS/qTCRhbPRDjg73BdWbdkFpFWge8G/FgkYxr9LBE1TsrxptppO9xoA5jXwJVZaWndSMvo6TuOjUgqY2w5RTkqhA=="; + + var xmlDoc = XmlHelpers.FromString(signedWithoutReference); + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .And.Message.Should().Be("No reference found in Xml signature, it doesn't validate the Xml data."); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsOnDualReferencesInSignature() + { + var xml = ""; + + var xmlDoc = XmlHelpers.FromString(xml); + + var signedXml = new SignedXml(xmlDoc); + signedXml.SigningKey = (RSACryptoServiceProvider)SignedXmlHelper.TestCert.PrivateKey; + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + + var ref1 = new Reference { Uri = "#myxml" }; + ref1.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + ref1.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(ref1); + + var ref2 = new Reference { Uri = "#myxml" }; + ref2.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + ref2.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(ref2); + + signedXml.ComputeSignature(); + xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(signedXml.GetXml(), true)); + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .And.Message.Should().Be("Multiple references for Xml signatures are not allowed."); + } + + [TestMethod] + public void XmlDocumentSigningExtensions_Sign_Nullcheck_doc() + { + XmlDocument dd = null; + dd.Invoking( + x => x.SignDocument(TestCert, MessageSigningAlgorithm.RsaSecureHashAlgorithm1)) + .ShouldThrow() + .And.ParamName.Should().Be("xmlDocument"); + } + + + [TestMethod] + public void XmlDocumentSigningExtensions_Sign_Nullcheck_Cert() + { + xmlDocument.Invoking( + x => x.SignDocument(null, MessageSigningAlgorithm.RsaSecureHashAlgorithm1)) + .ShouldThrow() + .And.ParamName.Should().Be("signingCertificate"); + } + + + [TestMethod] + public void XmlDocumentSigningExtensions_Sign_Nullcheck_DocElem() + { + if (xmlDocument.DocumentElement != null) xmlDocument.RemoveChild(xmlDocument.DocumentElement); + + xmlDocument.Invoking( + x => x.SignDocument(TestCert, MessageSigningAlgorithm.RsaSecureHashAlgorithm256)) + .ShouldThrow() + .And.ParamName.Should().Be("xmlDocument"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsInformativeMessageOnSha256Signature() + { + // With .Net 4.6.2 and above this test will not throw any error because the SHA256 is now built-in + if ( CryptoConfig.CreateFromName( "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" ) != null ) return; + + var xmlSignedWithSha256 = @"https://idp.example.comF+E7u3vqMC07ipvP9AowsMqP7y6CsAC0GeEIxNSwDEI=GmiXn24Ccnr64TbmDd1/nLM+891z0FtRHSpU8+75uOqbpNK/ZZGrltFf2YZ5u9b9O0HfbFFsZ0i28ocwAZOv2UfxQrCtOGf3ss7Q+t2Zmc6Q/3ES7HIa15I5BbaSdNfpOMlX6N1XXhMprRGy2YWMr5IAIhysFG1A2oHaC3yFiesfUrawN/lXUYuI22Kf4A5bmnIkKijnwX9ewnhRj6569bw+c6q+tVZSHQzI+KMU9KbKN4NsXxAmv6dM1w2qOiX9/CO9LzwEtlhA9yo3sl0uWP8z5GwK9qgOlsF2NdImAQ5f0U4Uv26doFn09W+VExFwNhcXhewQUuPBYBr+XXzdww==MIIDIzCCAg+gAwIBAgIQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAMCQxIjAgBgNVBAMTGUtlbnRvci5BdXRoU2VydmljZXMuVGVzdHMwHhcNMTMwOTI1MTMzNTQ0WhcNMzkxMjMxMjM1OTU5WjAkMSIwIAYDVQQDExlLZW50b3IuQXV0aFNlcnZpY2VzLlRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVGpfvK9N//MnA5Jo1q2liyPR24406Dp25gv7LB3HK4DWgqsb7xXM6KIV/WVOyCV2g/O1ErBlB+HLhVZ4XUJvbqBbgAJqFO+TZwcCIe8u4nTEXeU660FdtkKClA17sbtMrAGdDfOPwVBHSuavdHeD7jHNI4RUDGKnEW13/0EvnHDilIetwODRxrX/+41R24sJThFbMczByS3OAL2dcIxoAynaGeM90gXsVYow1QhJUy21+cictikb7jW4mW6dvFCBrWIceom9J295DcQIHoxJy5NoZwMir/JV00qs1wDVoN20Ve1DC5ImwcG46XPF7efQ44yLh2j5Yexw+xloA81dwIDAQABo1kwVzBVBgNVHQEETjBMgBAWIahoZhXVUogbAqkS7zwfoSYwJDEiMCAGA1UEAxMZS2VudG9yLkF1dGhTZXJ2aWNlcy5UZXN0c4IQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAA4IBAQA2aGzmuKw4AYXWMhrGj5+i8vyAoifUn1QVOFsUukEA77CrqhqqaWFoeagfJp/45vlvrfrEwtF0QcWfmO9w1VvHwm7sk1G/cdYyJ71sU+llDsdPZm7LxQvWZYkK+xELcinQpSwt4ExavS+jLcHoOYHYwIZMBn3U8wZw7Kq29oGnoFQz7HLCEl/G9i3QRyvFITNlWTjoScaqMjHTzq6HCMaRsL09DLcY3KB+cedfpC0/MBlzaxZv0DctTulyaDfM9DCYOyokGN/rQ6qkAR0DDm8fVwknbJY7kURXNGoUetulTb5ow8BvD1gncOaYHSD0kbHZG+bLsUZDFatEr2KW8jbGSomeUser"; + + var xmlDoc = XmlHelpers.FromString(xmlSignedWithSha256); + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCertSignOnly)) + .ShouldThrow() + .And.Message.Should().Be("SHA256 signatures require the algorithm to be registered at the process level. Upgrade to .Net 4.6.2 or call Kentor.AuthServices.Configuration.Options.GlobalEnableSha256XmlSignatures() on startup to register."); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsOnTamperedData_WithSha256Signature() + { + Options.GlobalEnableSha256XmlSignatures(); + + var xml = @"https://idp.example.comBKRyWqweAczLA8fgRcx6zzMDiP0qT0TwqU/X4VgLiXM=iK8s+MkLlixSSQu5Q/SHRZLhfnj4jlyPLAD6C2n9zmQu4CosZME7mxiNFiWyOE8XRGd+2LJle+NjJrkZFktVb03JaToq7w4Q8GfJ2oUUjNCweoaJ6NzsnwkFoXhyh0dfOixl/Ifa3qDX50/Hv2twF/QXfDs08GZTxZKehKsVDITyVd6nytF8VUb0+nU7UMWPn1XeHM7YNI/1mkVbCRx/ci5ZRxwjAX40xttd4JL6oBnp5oaaMgWpAa2cVb+t/9HhCRThEho1etbPHx/+E9ElL1PhKqKX6nh2GSH1TFJkwEXIPPZKqCs3YDINLBZpLfl626zbV4cGOGyWUAroVsk2uw==MIIDIzCCAg+gAwIBAgIQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAMCQxIjAgBgNVBAMTGUtlbnRvci5BdXRoU2VydmljZXMuVGVzdHMwHhcNMTMwOTI1MTMzNTQ0WhcNMzkxMjMxMjM1OTU5WjAkMSIwIAYDVQQDExlLZW50b3IuQXV0aFNlcnZpY2VzLlRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVGpfvK9N//MnA5Jo1q2liyPR24406Dp25gv7LB3HK4DWgqsb7xXM6KIV/WVOyCV2g/O1ErBlB+HLhVZ4XUJvbqBbgAJqFO+TZwcCIe8u4nTEXeU660FdtkKClA17sbtMrAGdDfOPwVBHSuavdHeD7jHNI4RUDGKnEW13/0EvnHDilIetwODRxrX/+41R24sJThFbMczByS3OAL2dcIxoAynaGeM90gXsVYow1QhJUy21+cictikb7jW4mW6dvFCBrWIceom9J295DcQIHoxJy5NoZwMir/JV00qs1wDVoN20Ve1DC5ImwcG46XPF7efQ44yLh2j5Yexw+xloA81dwIDAQABo1kwVzBVBgNVHQEETjBMgBAWIahoZhXVUogbAqkS7zwfoSYwJDEiMCAGA1UEAxMZS2VudG9yLkF1dGhTZXJ2aWNlcy5UZXN0c4IQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAA4IBAQA2aGzmuKw4AYXWMhrGj5+i8vyAoifUn1QVOFsUukEA77CrqhqqaWFoeagfJp/45vlvrfrEwtF0QcWfmO9w1VvHwm7sk1G/cdYyJ71sU+llDsdPZm7LxQvWZYkK+xELcinQpSwt4ExavS+jLcHoOYHYwIZMBn3U8wZw7Kq29oGnoFQz7HLCEl/G9i3QRyvFITNlWTjoScaqMjHTzq6HCMaRsL09DLcY3KB+cedfpC0/MBlzaxZv0DctTulyaDfM9DCYOyokGN/rQ6qkAR0DDm8fVwknbJY7kURXNGoUetulTb5ow8BvD1gncOaYHSD0kbHZG+bLsUZDFatEr2KW8jbGSomeUser"; + xml = xml.Replace("SomeUser", "OtherUser"); + + var xmlDoc = XmlHelpers.FromString(xml); + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .WithMessage("Signature didn't verify. Have the contents been tampered with?"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_ThrowsOnIncorrectTransformsInSignature() + { + // SAML2 Core 5.4.4 states that signatures SHOULD NOT contain other transforms than + // the enveloped signature or exclusive canonicalization transforms and that a verifier + // of a signature MAY reject signatures with other transforms. We'll reject them to + // mitigate the risk of transforms opening up for assertion injections. + + var xml = ""; + + var xmlDoc = XmlHelpers.FromString(xml); + + var signedXml = new SignedXml(xmlDoc); + signedXml.SigningKey = (RSACryptoServiceProvider)SignedXmlHelper.TestCert.PrivateKey; + signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; + + var reference = new Reference { Uri = "#MyXml" }; + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigC14NTransform()); // The allowed transform is XmlDsigExcC14NTransform + signedXml.AddReference(reference); + + signedXml.ComputeSignature(); + xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(signedXml.GetXml(), true)); + + xmlDoc.DocumentElement.Invoking( + x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .And.Message.Should().Be("Transform \"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\" found in Xml signature SHOULD NOT be used with SAML2."); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_HandlesMultipleSignaturesInSameDocument() + { + var content1 = SignedXmlHelper.SignXml(""); + var content2 = SignedXmlHelper.SignXml(""); + + var xml = +$@" + {content1} + {content2} +"; + + var xmlDoc = XmlHelpers.FromString(xml); + + ((XmlElement)xmlDoc.SelectSingleNode("//*[@ID=\"c1\"]")).IsSignedBy(SignedXmlHelper.TestCert) + .Should().BeTrue("first content is correclty signed"); + + ((XmlElement)xmlDoc.SelectSingleNode("//*[@ID=\"c2\"]")).IsSignedBy(SignedXmlHelper.TestCert) + .Should().BeTrue("second content is correclty signed"); + } + + [TestMethod] + public void XmlHelpers_IsSignedBy_DoesNotThrowSha256MessageForOtherProblem() + { + Options.GlobalEnableSha256XmlSignatures(); + + // Here I've specified an invalid digest algorithm. Want to be sure it + // does NOT flag this as a problem with the sha256 signature algorithm + // (they both throw CryptographicException) + var xml = + @"https://idp.example.comF+E7u3vqMC07ipvP9AowsMqP7y6CsAC0GeEIxNSwDEI=GmiXn24Ccnr64TbmDd1/nLM+891z0FtRHSpU8+75uOqbpNK/ZZGrltFf2YZ5u9b9O0HfbFFsZ0i28ocwAZOv2UfxQrCtOGf3ss7Q+t2Zmc6Q/3ES7HIa15I5BbaSdNfpOMlX6N1XXhMprRGy2YWMr5IAIhysFG1A2oHaC3yFiesfUrawN/lXUYuI22Kf4A5bmnIkKijnwX9ewnhRj6569bw+c6q+tVZSHQzI+KMU9KbKN4NsXxAmv6dM1w2qOiX9/CO9LzwEtlhA9yo3sl0uWP8z5GwK9qgOlsF2NdImAQ5f0U4Uv26doFn09W+VExFwNhcXhewQUuPBYBr+XXzdww==MIIDIzCCAg+gAwIBAgIQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAMCQxIjAgBgNVBAMTGUtlbnRvci5BdXRoU2VydmljZXMuVGVzdHMwHhcNMTMwOTI1MTMzNTQ0WhcNMzkxMjMxMjM1OTU5WjAkMSIwIAYDVQQDExlLZW50b3IuQXV0aFNlcnZpY2VzLlRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVGpfvK9N//MnA5Jo1q2liyPR24406Dp25gv7LB3HK4DWgqsb7xXM6KIV/WVOyCV2g/O1ErBlB+HLhVZ4XUJvbqBbgAJqFO+TZwcCIe8u4nTEXeU660FdtkKClA17sbtMrAGdDfOPwVBHSuavdHeD7jHNI4RUDGKnEW13/0EvnHDilIetwODRxrX/+41R24sJThFbMczByS3OAL2dcIxoAynaGeM90gXsVYow1QhJUy21+cictikb7jW4mW6dvFCBrWIceom9J295DcQIHoxJy5NoZwMir/JV00qs1wDVoN20Ve1DC5ImwcG46XPF7efQ44yLh2j5Yexw+xloA81dwIDAQABo1kwVzBVBgNVHQEETjBMgBAWIahoZhXVUogbAqkS7zwfoSYwJDEiMCAGA1UEAxMZS2VudG9yLkF1dGhTZXJ2aWNlcy5UZXN0c4IQg7mOjTf994NAVxZu4jqXpzAJBgUrDgMCHQUAA4IBAQA2aGzmuKw4AYXWMhrGj5+i8vyAoifUn1QVOFsUukEA77CrqhqqaWFoeagfJp/45vlvrfrEwtF0QcWfmO9w1VvHwm7sk1G/cdYyJ71sU+llDsdPZm7LxQvWZYkK+xELcinQpSwt4ExavS+jLcHoOYHYwIZMBn3U8wZw7Kq29oGnoFQz7HLCEl/G9i3QRyvFITNlWTjoScaqMjHTzq6HCMaRsL09DLcY3KB+cedfpC0/MBlzaxZv0DctTulyaDfM9DCYOyokGN/rQ6qkAR0DDm8fVwknbJY7kURXNGoUetulTb5ow8BvD1gncOaYHSD0kbHZG+bLsUZDFatEr2KW8jbGSomeUser"; + + var xmlDoc = XmlHelpers.FromString(xml); + + xmlDoc.DocumentElement.Invoking(x => x.IsSignedBy(SignedXmlHelper.TestCert)) + .ShouldThrow() + .WithMessage("SignatureDescription could not be created for the signature algorithm supplied."); + } + + [TestMethod] + public void XmlHelpers_IsSignedByAny_ThrowsOnCertValidationWithRsaKey() + { + var xml = ""; + + var xmlDoc = XmlHelpers.FromString(xml); + xmlDoc.Sign(SignedXmlHelper.TestCert, false); + + var signingKeys = Enumerable.Repeat(SignedXmlHelper.TestKey, 1); + + xmlDoc.DocumentElement.Invoking(x => x.IsSignedByAny(signingKeys, true)) + .ShouldThrow() + .And.Message.Should().Be("Certificate validation enabled, but the signing key identifier is of type RsaKeyIdentifierClause which cannot be validated as a certificate."); + } + } } \ No newline at end of file diff --git a/Kentor.AuthServices/Configuration/IdentityProviderElement.cs b/Kentor.AuthServices/Configuration/IdentityProviderElement.cs index af5d1634e..d67e54239 100644 --- a/Kentor.AuthServices/Configuration/IdentityProviderElement.cs +++ b/Kentor.AuthServices/Configuration/IdentityProviderElement.cs @@ -1,6 +1,8 @@ using Kentor.AuthServices.WebSso; using System; using System.Configuration; +using System.Data.Odbc; +using Kentor.AuthServices.Saml2P; namespace Kentor.AuthServices.Configuration { @@ -104,6 +106,39 @@ internal set base["signingCertificate"] = value; } } + const string useSpecificAuthenticateRequestSigningAlgorithm = nameof(useSpecificAuthenticateRequestSigningAlgorithm); + /// + /// The authenticateRequestSigningAlgorithm. + /// + [ConfigurationProperty(useSpecificAuthenticateRequestSigningAlgorithm, IsRequired = false, DefaultValue = false)] + public bool UseSpecificAuthenticateRequestSigningAlgorithm + { + get + { + return (bool)base[useSpecificAuthenticateRequestSigningAlgorithm]; + } + //internal set + //{ + // base[authenticateRequestSigningAlgorithm] = value; + //} + } + + const string authenticateRequestSigningAlgorithm = nameof(authenticateRequestSigningAlgorithm); + /// + /// The authenticateRequestSigningAlgorithm. + /// + [ConfigurationProperty(authenticateRequestSigningAlgorithm, IsRequired = false, DefaultValue = MessageSigningDefaults.DefaultAlgorithm)] + public MessageSigningAlgorithm AuthenticateRequestSigningAlgorithm + { + get + { + return (MessageSigningAlgorithm)base[authenticateRequestSigningAlgorithm]; + } + //internal set + //{ + // base[authenticateRequestSigningAlgorithm] = value; + //} + } /// /// Allow unsolicited responses. That is InResponseTo is missing in the AuthnRequest. diff --git a/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs b/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs index 8933b98da..b2ea75b01 100644 --- a/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs +++ b/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs @@ -315,12 +315,24 @@ internal set base[authenticateRequestSigningBehavior] = value; } } + const string defaultAuthenticateRequestSigningAlgorithm = nameof(defaultAuthenticateRequestSigningAlgorithm); + /// + /// The default AuthenticateRequest signing Algorithm. + /// + [ConfigurationProperty(defaultAuthenticateRequestSigningAlgorithm, IsRequired = false, DefaultValue = MessageSigningDefaults.DefaultAlgorithm)] + public MessageSigningAlgorithm DefaultAuthenticateRequestSigningAlgorithm + { + get + { + return (MessageSigningAlgorithm)base[defaultAuthenticateRequestSigningAlgorithm]; + } + } const string validateCertificates = nameof(validateCertificates); /// /// Validate certificates when validating signatures? Normally not a /// good idea as SAML2 deployments typically exchange certificates - /// directly and isntead of relying on the public certificate + /// directly and instead of relying on the public certificate /// infrastructure. /// [ConfigurationProperty(validateCertificates, IsRequired = false)] diff --git a/Kentor.AuthServices/Configuration/SPOptions.cs b/Kentor.AuthServices/Configuration/SPOptions.cs index 1c323e93c..727d02b43 100644 --- a/Kentor.AuthServices/Configuration/SPOptions.cs +++ b/Kentor.AuthServices/Configuration/SPOptions.cs @@ -28,6 +28,7 @@ public SPOptions() systemIdentityModelIdentityConfiguration = new IdentityConfiguration(false); MetadataCacheDuration = new TimeSpan(1, 0, 0); Compatibility = new Compatibility(); + DefaultAuthenticateRequestSigningAlgorithm = MessageSigningDefaults.DefaultAlgorithm; } /// @@ -53,6 +54,7 @@ public SPOptions(KentorAuthServicesSection configSection) PublicOrigin = configSection.PublicOrigin; Organization = configSection.Organization; AuthenticateRequestSigningBehavior = configSection.AuthenticateRequestSigningBehavior; + DefaultAuthenticateRequestSigningAlgorithm = configSection.DefaultAuthenticateRequestSigningAlgorithm; NameIdPolicy = new Saml2NameIdPolicy( configSection.NameIdPolicyElement.AllowCreate, configSection.NameIdPolicyElement.Format); RequestedAuthnContext = new Saml2RequestedAuthnContext(configSection.RequestedAuthnContext); @@ -352,7 +354,10 @@ private IEnumerable publishableServiceCertificates /// Signing behaviour for AuthnRequests. /// public SigningBehavior AuthenticateRequestSigningBehavior { get; set; } - + /// + /// The default algorithm to use when signing Authentication Requests + /// + public MessageSigningAlgorithm DefaultAuthenticateRequestSigningAlgorithm { get; set; } /// /// Metadata flag that we want assertions to be signed. /// diff --git a/Kentor.AuthServices/IdentityProvider.cs b/Kentor.AuthServices/IdentityProvider.cs index 0ba2c141a..bf0539bb9 100644 --- a/Kentor.AuthServices/IdentityProvider.cs +++ b/Kentor.AuthServices/IdentityProvider.cs @@ -35,6 +35,8 @@ public IdentityProvider(EntityId entityId, SPOptions spOptions) { EntityId = entityId; this.spOptions = spOptions; + if (null!=spOptions) + this.SigningAlgorithm = spOptions.DefaultAuthenticateRequestSigningAlgorithm; } readonly SPOptions spOptions; @@ -58,6 +60,10 @@ internal IdentityProvider(IdentityProviderElement config, SPOptions spOptions) new X509RawDataKeyIdentifierClause(certificate)); } + SigningAlgorithm = config.UseSpecificAuthenticateRequestSigningAlgorithm + ? config.AuthenticateRequestSigningAlgorithm + : spOptions.DefaultAuthenticateRequestSigningAlgorithm; + foreach (var ars in config.ArtifactResolutionServices) { ArtifactResolutionServiceUrls[ars.Index] = ars.Location; @@ -291,7 +297,7 @@ public string MetadataLocation AttributeConsumingServiceIndex = spOptions.AttributeConsumingServices.Any() ? 0 : (int?)null, NameIdPolicy = spOptions.NameIdPolicy, RequestedAuthnContext = spOptions.RequestedAuthnContext, - SigningAlgorithm = SigningAlgorithm + SigningAlgorithm = this.SigningAlgorithm }; if (spOptions.AuthenticateRequestSigningBehavior == SigningBehavior.Always @@ -308,7 +314,6 @@ public string MetadataLocation } authnRequest.SigningCertificate = spOptions.SigningServiceCertificate; - authnRequest.SigningAlgorithm = this.SigningAlgorithm; } return authnRequest; @@ -508,6 +513,7 @@ public Saml2LogoutRequest CreateLogoutRequest(ClaimsPrincipal user) SessionIndex = user.FindFirst(AuthServicesClaimTypes.SessionIndex).Value, SigningCertificate = spOptions.SigningServiceCertificate, + SigningAlgorithm = this.SigningAlgorithm }; } diff --git a/Kentor.AuthServices/MessageSigningAlgorithm.cs b/Kentor.AuthServices/MessageSigningAlgorithm.cs index c2acc02f9..0f0803f3c 100644 --- a/Kentor.AuthServices/MessageSigningAlgorithm.cs +++ b/Kentor.AuthServices/MessageSigningAlgorithm.cs @@ -23,4 +23,16 @@ public enum MessageSigningAlgorithm RsaSecureHashAlgorithm512 } + + /// + /// Class MessageSigningDefaults. + /// + public static class MessageSigningDefaults + { + /// + /// The default algorithm + /// + public const MessageSigningAlgorithm DefaultAlgorithm = + MessageSigningAlgorithm.RsaSecureHashAlgorithm1; + } } \ No newline at end of file diff --git a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs index 998c0aa38..2ff0d3bc5 100644 --- a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs +++ b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs @@ -55,7 +55,7 @@ protected Saml2StatusResponseType(Saml2StatusCode status) /// public X509Certificate2 SigningCertificate { get; set; } - public MessageSigningAlgorithm SigningAlgorithm { get; set; } + public MessageSigningAlgorithm SigningAlgorithm { get; } /// /// Status code of the message. diff --git a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs index 7bf1df84c..eee60217a 100644 --- a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs +++ b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs @@ -1,11 +1,9 @@ using Kentor.AuthServices.Configuration; using Kentor.AuthServices.Saml2P; using System; -using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; -using System.Web; using System.Xml; namespace Kentor.AuthServices.WebSso diff --git a/Kentor.AuthServices/XmlDocumentSigningExtensions.cs b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs index 70bf57eb4..6a84c6651 100644 --- a/Kentor.AuthServices/XmlDocumentSigningExtensions.cs +++ b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs @@ -14,7 +14,7 @@ namespace Kentor.AuthServices /// public static class XmlDocumentSigningExtensions { - private static readonly Dictionary _algorithmToNamespaceMap = new Dictionary + private static readonly Dictionary AlgorithmToNamespaceMap = new Dictionary { { MessageSigningAlgorithm.RsaSecureHashAlgorithm1,RSASHA1}, { MessageSigningAlgorithm.RsaSecureHashAlgorithm256,RSASHA256}, @@ -37,29 +37,23 @@ public static class XmlDocumentSigningExtensions /// public static string ToNamespace(this MessageSigningAlgorithm algorithm) { - return _algorithmToNamespaceMap[algorithm]; + return AlgorithmToNamespaceMap[algorithm]; } - /// - /// Add digital signature to an xml document. - /// The default signing algorithm RSA SHA-256 is used in this method. - /// - /// - /// - public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate) + public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, MessageSigningAlgorithm signingAlgorithm) { - SignDocument(xmlDocument, signingCertificate, MessageSigningAlgorithm.RsaSecureHashAlgorithm256); + SignDocument(xmlDocument, signingCertificate, signingAlgorithm, true); } - /// /// Add digital signature to an xml document. /// /// The XML document. /// The signing certificate. /// The signing algorithm. + /// Whether to include key info clause in the resulting document /// /// - public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, MessageSigningAlgorithm algorithm) + public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, MessageSigningAlgorithm algorithm, bool includeKeyInfo) { if (xmlDocument == null) { @@ -77,7 +71,7 @@ public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 s string signatureMethodNamespace = algorithm.ToNamespace(); - // Note that this will return a Basic crypto provider, with only SHA-1 support + // Note that this will return a Basic cryptoprovider, with only SHA-1 support var key = (RSACryptoServiceProvider) signingCertificate.PrivateKey; using (var provider = new RSACryptoServiceProvider()) @@ -85,8 +79,7 @@ public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 s CspKeyContainerInfo enhCsp = provider.CspKeyContainerInfo; using (key = new RSACryptoServiceProvider(new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, key.CspKeyContainerInfo.KeyContainerName))) { - var signedXml = new SignedXml(xmlDocument); - signedXml.SigningKey = key; + var signedXml = new SignedXml(xmlDocument) {SigningKey = key}; signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; signedXml.SignedInfo.SignatureMethod = signatureMethodNamespace; @@ -94,6 +87,14 @@ public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 s reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); reference.AddTransform(new XmlDsigExcC14NTransform()); signedXml.AddReference(reference); + + if (includeKeyInfo) + { + var keyInfo = new KeyInfo(); + keyInfo.AddClause(new KeyInfoX509Data(signingCertificate)); + signedXml.KeyInfo = keyInfo; + } + signedXml.ComputeSignature(); xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signedXml.GetXml(), true)); } From d908a13abe8324f6fb0eac79cad83d65864d7c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20T=C3=B8nners?= Date: Wed, 9 Nov 2016 17:54:39 +0100 Subject: [PATCH 3/6] Fix test - omit SigningAlgorithm in Saml2Response_Read_BasicParams --- Kentor.AuthServices.Tests/Saml2P/Saml2ResponseTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Kentor.AuthServices.Tests/Saml2P/Saml2ResponseTests.cs b/Kentor.AuthServices.Tests/Saml2P/Saml2ResponseTests.cs index de64fe338..57f18120e 100644 --- a/Kentor.AuthServices.Tests/Saml2P/Saml2ResponseTests.cs +++ b/Kentor.AuthServices.Tests/Saml2P/Saml2ResponseTests.cs @@ -60,6 +60,7 @@ public void Saml2Response_Read_BasicParams() expected, opt => opt .Excluding(s => s.XmlElement) .Excluding(s => s.SigningCertificate) + .Excluding(s => s.SigningAlgorithm) .Excluding(s => s.SessionNotOnOrAfter)); } From 0de4e89c4c943aaa0acaf91efb06d0708f40c6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20T=C3=B8nners?= Date: Wed, 14 Dec 2016 14:49:49 +0100 Subject: [PATCH 4/6] Reset code formatting in Saml2PostBinding --- .../WebSSO/Saml2PostBinding.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs index eee60217a..d89be5e1b 100644 --- a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs +++ b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Text; +using System.Web; using System.Xml; namespace Kentor.AuthServices.WebSso @@ -50,13 +51,13 @@ public override UnbindResult Unbind(HttpRequestData request, IOptions options) public override CommandResult Bind(ISaml2Message message) { - if (message == null) + if(message == null) { throw new ArgumentNullException(nameof(message)); } var xml = message.ToXml(); - if (message.SigningCertificate != null) + if(message.SigningCertificate != null) { var xmlDoc = new XmlDocument() { @@ -79,9 +80,20 @@ public override CommandResult Bind(ISaml2Message message) var encodedXml = Convert.ToBase64String(Encoding.UTF8.GetBytes(xml)); - var relayStateHtml = string.IsNullOrEmpty(message.RelayState) ? null : string.Format(CultureInfo.InvariantCulture, PostHtmlRelayStateFormatString, message.RelayState); + var relayStateHtml = string.IsNullOrEmpty(message.RelayState) ? null + : string.Format(CultureInfo.InvariantCulture, PostHtmlRelayStateFormatString, message.RelayState); - var cr = new CommandResult() {ContentType = "text/html", Content = String.Format(CultureInfo.InvariantCulture, PostHtmlFormatString, message.DestinationUrl, relayStateHtml, message.MessageName, encodedXml)}; + var cr = new CommandResult() + { + ContentType = "text/html", + Content = String.Format( + CultureInfo.InvariantCulture, + PostHtmlFormatString, + message.DestinationUrl, + relayStateHtml, + message.MessageName, + encodedXml) + }; return cr; } From 1425070c7bf27ef652e61be45e146f872e79bd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20T=C3=B8nners?= Date: Wed, 14 Dec 2016 15:46:00 +0100 Subject: [PATCH 5/6] Change MessageSigningAlgorithm to String type --- .../IdentityProviderTests.cs | 3 + .../WebSSO/Saml2MessageImplementation.cs | 2 +- .../WebSSO/Saml2PostBindingTests.cs | 417 +++++++++--------- .../Configuration/IdentityProviderElement.cs | 4 +- .../KentorAuthServicesSection.cs | 4 +- .../Configuration/SPOptions.cs | 2 +- Kentor.AuthServices/IdentityProvider.cs | 2 +- .../MessageSigningAlgorithm.cs | 12 +- Kentor.AuthServices/SAML2P/ISaml2Message.cs | 2 +- .../SAML2P/Saml2RequestBase.cs | 7 +- Kentor.AuthServices/SAML2P/Saml2Response.cs | 8 +- .../SAML2P/Saml2StatusResponseType.cs | 7 +- .../WebSSO/Saml2RedirectBinding.cs | 2 +- .../XmlDocumentSigningExtensions.cs | 23 +- 14 files changed, 263 insertions(+), 232 deletions(-) diff --git a/Kentor.AuthServices.Tests/IdentityProviderTests.cs b/Kentor.AuthServices.Tests/IdentityProviderTests.cs index 3237adf3c..30ad8e958 100644 --- a/Kentor.AuthServices.Tests/IdentityProviderTests.cs +++ b/Kentor.AuthServices.Tests/IdentityProviderTests.cs @@ -87,6 +87,7 @@ public void IdentityProvider_CreateAuthenticateRequest_BasicInfo() subject.ShouldBeEquivalentTo(expected, opt => opt .Excluding(au => au.Id) + .Excluding(au=>au.SigningAlgorithm) .Excluding(au => au.RelayState)); subject.RelayState.Should().HaveLength(56); @@ -113,6 +114,7 @@ public void IdentityProvider_CreateAuthenticateRequest_PublicOrigin() subject.ShouldBeEquivalentTo(expected, opt => opt .Excluding(au => au.Id) + .Excluding(au => au.SigningAlgorithm) .Excluding(au => au.RelayState)); } @@ -137,6 +139,7 @@ public void IdentityProvider_CreateAuthenticateRequest_NoAttributeIndex() subject.ShouldBeEquivalentTo(expected, opt => opt .Excluding(au => au.Id) + .Excluding(au => au.SigningAlgorithm) .Excluding(au => au.RelayState)); } diff --git a/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs b/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs index 39c2fb33a..f3847337e 100644 --- a/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs +++ b/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs @@ -25,7 +25,7 @@ public string ToXml() public string XmlData { get; set; } public X509Certificate2 SigningCertificate { get; set; } - public MessageSigningAlgorithm SigningAlgorithm { get; } + public string SigningAlgorithm { get; set; } public EntityId Issuer { get; set; } } diff --git a/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs b/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs index 18d40dd37..699cb2e0d 100644 --- a/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs +++ b/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs @@ -1,104 +1,106 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Web; -using FluentAssertions; -using System.Collections.Specialized; -using System.IdentityModel.Tokens; -using System.Xml; -using System.Text; -using System.Collections.Generic; -using Kentor.AuthServices.WebSso; -using Kentor.AuthServices.Tests.WebSSO; -using Kentor.AuthServices.Tests.Helpers; - -namespace Kentor.AuthServices.Tests.WebSso -{ - [TestClass] - public class Saml2PostBindingTests - { - private HttpRequestData CreateRequest(string encodedResponse, string relayState = null) - { - var formData = new List>() - { - new KeyValuePair("SAMLResponse", new string[] {encodedResponse }) - }; - - if (!string.IsNullOrEmpty(relayState)) - { - formData.Add(new KeyValuePair("RelayState", new string[] { relayState })); - }; - - return new HttpRequestData( - "POST", - new Uri("http://example.com"), - "/ModulePath", - formData, - null); - } - - [TestMethod] - public void Saml2PostBinding_Unbind_Nullcheck() - { - Saml2Binding.Get(Saml2BindingType.HttpPost) - .Invoking(b => b.Unbind(null, null)) - .ShouldThrow().And.ParamName.Should().Be("request"); - } - - [TestMethod] - public void Saml2PostBinding_Unbind_ThrowsOnNotBase64Encoded() - { - Saml2Binding.Get(Saml2BindingType.HttpPost) - .Invoking(b => b.Unbind(CreateRequest("foo"), null)) - .ShouldThrow(); - } - - [TestMethod] - public void Saml2PostBinding_Unbind_ReadsSaml2Response() - { - string response = ""; - - var r = CreateRequest(Convert.ToBase64String(Encoding.UTF8.GetBytes(response))); - - Saml2Binding.Get(Saml2BindingType.HttpPost).Unbind(r, null).Data.OuterXml.Should().Be(response); - } - - [TestMethod] - public void Saml2PostBinding_Unbind_ReadsRelayState() - { - string response = ""; - string relayState = "someState"; - - var r = CreateRequest( - Convert.ToBase64String(Encoding.UTF8.GetBytes(response)), - relayState); - - Saml2Binding.Get(Saml2BindingType.HttpPost) - .Unbind(r, null).RelayState.Should().Be(relayState); - } - - [TestMethod] - public void Saml2PostBinding_Bind_Nullcheck() - { - Saml2Binding.Get(Saml2BindingType.HttpPost) - .Invoking(b => b.Bind(null)) - .ShouldThrow().And.ParamName.Should().Be("message"); - } - - [TestMethod] - public void Saml2PostBinding_Bind() - { - var message = new Saml2MessageImplementation - { - XmlData = "data", - DestinationUrl = new Uri("http://www.example.com/acs"), - MessageName = "SAMLMessageName" - }; - - var result = Saml2Binding.Get(Saml2BindingType.HttpPost).Bind(message); - - var expected = new CommandResult() - { - ContentType = "text/html", +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Web; +using FluentAssertions; +using System.Collections.Specialized; +using System.IdentityModel.Tokens; +using System.Xml; +using System.Text; +using System.Collections.Generic; +using IdentityServer3.Core; +using Kentor.AuthServices.Saml2P; +using Kentor.AuthServices.WebSso; +using Kentor.AuthServices.Tests.WebSSO; +using Kentor.AuthServices.Tests.Helpers; + +namespace Kentor.AuthServices.Tests.WebSso +{ + [TestClass] + public class Saml2PostBindingTests + { + private HttpRequestData CreateRequest(string encodedResponse, string relayState = null) + { + var formData = new List>() + { + new KeyValuePair("SAMLResponse", new string[] {encodedResponse }) + }; + + if (!string.IsNullOrEmpty(relayState)) + { + formData.Add(new KeyValuePair("RelayState", new string[] { relayState })); + }; + + return new HttpRequestData( + "POST", + new Uri("http://example.com"), + "/ModulePath", + formData, + null); + } + + [TestMethod] + public void Saml2PostBinding_Unbind_Nullcheck() + { + Saml2Binding.Get(Saml2BindingType.HttpPost) + .Invoking(b => b.Unbind(null, null)) + .ShouldThrow().And.ParamName.Should().Be("request"); + } + + [TestMethod] + public void Saml2PostBinding_Unbind_ThrowsOnNotBase64Encoded() + { + Saml2Binding.Get(Saml2BindingType.HttpPost) + .Invoking(b => b.Unbind(CreateRequest("foo"), null)) + .ShouldThrow(); + } + + [TestMethod] + public void Saml2PostBinding_Unbind_ReadsSaml2Response() + { + string response = ""; + + var r = CreateRequest(Convert.ToBase64String(Encoding.UTF8.GetBytes(response))); + + Saml2Binding.Get(Saml2BindingType.HttpPost).Unbind(r, null).Data.OuterXml.Should().Be(response); + } + + [TestMethod] + public void Saml2PostBinding_Unbind_ReadsRelayState() + { + string response = ""; + string relayState = "someState"; + + var r = CreateRequest( + Convert.ToBase64String(Encoding.UTF8.GetBytes(response)), + relayState); + + Saml2Binding.Get(Saml2BindingType.HttpPost) + .Unbind(r, null).RelayState.Should().Be(relayState); + } + + [TestMethod] + public void Saml2PostBinding_Bind_Nullcheck() + { + Saml2Binding.Get(Saml2BindingType.HttpPost) + .Invoking(b => b.Bind(null)) + .ShouldThrow().And.ParamName.Should().Be("message"); + } + + [TestMethod] + public void Saml2PostBinding_Bind() + { + var message = new Saml2MessageImplementation + { + XmlData = "data", + DestinationUrl = new Uri("http://www.example.com/acs"), + MessageName = "SAMLMessageName" + }; + + var result = Saml2Binding.Get(Saml2BindingType.HttpPost).Bind(message); + + var expected = new CommandResult() + { + ContentType = "text/html", Content = @" @@ -122,28 +124,28 @@ public void Saml2PostBinding_Bind() -" - }; - - result.ShouldBeEquivalentTo(expected); - } - - [TestMethod] - public void Saml2PostBinding_Bind_WithRelayState() - { - var message = new Saml2MessageImplementation - { - DestinationUrl = new Uri("http://www.example.com/acs"), - XmlData = "data", - MessageName = "SAMLMessageName", - RelayState = "ABC1234" - }; - - var result = Saml2Binding.Get(Saml2BindingType.HttpPost).Bind(message); - - var expected = new CommandResult() - { - ContentType = "text/html", +" + }; + + result.ShouldBeEquivalentTo(expected); + } + + [TestMethod] + public void Saml2PostBinding_Bind_WithRelayState() + { + var message = new Saml2MessageImplementation + { + DestinationUrl = new Uri("http://www.example.com/acs"), + XmlData = "data", + MessageName = "SAMLMessageName", + RelayState = "ABC1234" + }; + + var result = Saml2Binding.Get(Saml2BindingType.HttpPost).Bind(message); + + var expected = new CommandResult() + { + ContentType = "text/html", Content = @" @@ -168,32 +170,33 @@ public void Saml2PostBinding_Bind_WithRelayState() -" - }; - - result.ShouldBeEquivalentTo(expected); - } - - [TestMethod] - public void Saml2PostBinding_Bind_SignsXml() - { - var message = new Saml2MessageImplementation - { - DestinationUrl = new Uri("http://www.example.com/acs"), - XmlData = "data", - MessageName = "SAMLMessageName", - RelayState = "ABC1234", - SigningCertificate = SignedXmlHelper.TestCert - }; - - var signedXml = SignedXmlHelper.SignXml(message.XmlData, true); - var expectedValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(signedXml)); - - var result = Saml2Binding.Get(Saml2BindingType.HttpPost).Bind(message); - - var expected = new CommandResult() - { - ContentType = "text/html", +" + }; + + result.ShouldBeEquivalentTo(expected); + } + + [TestMethod] + public void Saml2PostBinding_Bind_SignsXml() + { + var message = new Saml2MessageImplementation + { + DestinationUrl = new Uri("http://www.example.com/acs"), + XmlData = "data", + MessageName = "SAMLMessageName", + RelayState = "ABC1234", + SigningCertificate = SignedXmlHelper.TestCert, + SigningAlgorithm = MessageSigningAlgorithm.RsaSecureHashAlgorithm1 + }; + + var signedXml = SignedXmlHelper.SignXml(message.XmlData, true); + var expectedValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(signedXml)); + + var result = Saml2Binding.Get(Saml2BindingType.HttpPost).Bind(message); + + var expected = new CommandResult() + { + ContentType = "text/html", Content = @" @@ -218,61 +221,61 @@ public void Saml2PostBinding_Bind_SignsXml() -" - }; - - result.ShouldBeEquivalentTo(expected); - } - - [TestMethod] - public void Saml2PostBinding_CanUnbind_Nullcheck() - { - Saml2Binding.Get(Saml2BindingType.HttpPost) - .Invoking(b => b.CanUnbind(null)) - .ShouldThrow().And.ParamName.Should().Be("request"); - } - - [TestMethod] - public void Saml2PostBinding_CanUnbind_DetectsRequests() - { - var requestData = Convert.ToBase64String(Encoding.UTF8.GetBytes("")); - - var request = new HttpRequestData( - "POST", - new Uri("http://something"), - "/path", - new KeyValuePair[] - { - new KeyValuePair("SAMLRequest", new[] { requestData }) - }, - null, - null); - - Saml2Binding.Get(Saml2BindingType.HttpPost) - .CanUnbind(request).Should().BeTrue(); - } - - [TestMethod] - public void Saml2PostBinding_Unbind_Request() - { - var requestData = Convert.ToBase64String(Encoding.UTF8.GetBytes("")); - - var request = new HttpRequestData( - "POST", - new Uri("http://something"), - "/path", - new KeyValuePair[] - { - new KeyValuePair("SAMLRequest", new[] { requestData }) - }, - null, - null); - - var actual = Saml2Binding.Get(request).Unbind(request, StubFactory.CreateOptions()); - - actual.Data.Should().BeEquivalentTo(XmlHelpers.FromString("").DocumentElement); - actual.RelayState.Should().BeNull(); - actual.TrustLevel.Should().Be(TrustLevel.None); - } - } -} +" + }; + + result.ShouldBeEquivalentTo(expected); + } + + [TestMethod] + public void Saml2PostBinding_CanUnbind_Nullcheck() + { + Saml2Binding.Get(Saml2BindingType.HttpPost) + .Invoking(b => b.CanUnbind(null)) + .ShouldThrow().And.ParamName.Should().Be("request"); + } + + [TestMethod] + public void Saml2PostBinding_CanUnbind_DetectsRequests() + { + var requestData = Convert.ToBase64String(Encoding.UTF8.GetBytes("")); + + var request = new HttpRequestData( + "POST", + new Uri("http://something"), + "/path", + new KeyValuePair[] + { + new KeyValuePair("SAMLRequest", new[] { requestData }) + }, + null, + null); + + Saml2Binding.Get(Saml2BindingType.HttpPost) + .CanUnbind(request).Should().BeTrue(); + } + + [TestMethod] + public void Saml2PostBinding_Unbind_Request() + { + var requestData = Convert.ToBase64String(Encoding.UTF8.GetBytes("")); + + var request = new HttpRequestData( + "POST", + new Uri("http://something"), + "/path", + new KeyValuePair[] + { + new KeyValuePair("SAMLRequest", new[] { requestData }) + }, + null, + null); + + var actual = Saml2Binding.Get(request).Unbind(request, StubFactory.CreateOptions()); + + actual.Data.Should().BeEquivalentTo(XmlHelpers.FromString("").DocumentElement); + actual.RelayState.Should().BeNull(); + actual.TrustLevel.Should().Be(TrustLevel.None); + } + } +} diff --git a/Kentor.AuthServices/Configuration/IdentityProviderElement.cs b/Kentor.AuthServices/Configuration/IdentityProviderElement.cs index d67e54239..25670fe5b 100644 --- a/Kentor.AuthServices/Configuration/IdentityProviderElement.cs +++ b/Kentor.AuthServices/Configuration/IdentityProviderElement.cs @@ -128,11 +128,11 @@ public bool UseSpecificAuthenticateRequestSigningAlgorithm /// The authenticateRequestSigningAlgorithm. /// [ConfigurationProperty(authenticateRequestSigningAlgorithm, IsRequired = false, DefaultValue = MessageSigningDefaults.DefaultAlgorithm)] - public MessageSigningAlgorithm AuthenticateRequestSigningAlgorithm + public string AuthenticateRequestSigningAlgorithm { get { - return (MessageSigningAlgorithm)base[authenticateRequestSigningAlgorithm]; + return (string)base[authenticateRequestSigningAlgorithm]; } //internal set //{ diff --git a/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs b/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs index b2ea75b01..3e903deaf 100644 --- a/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs +++ b/Kentor.AuthServices/Configuration/KentorAuthServicesSection.cs @@ -320,11 +320,11 @@ internal set /// The default AuthenticateRequest signing Algorithm. /// [ConfigurationProperty(defaultAuthenticateRequestSigningAlgorithm, IsRequired = false, DefaultValue = MessageSigningDefaults.DefaultAlgorithm)] - public MessageSigningAlgorithm DefaultAuthenticateRequestSigningAlgorithm + public string DefaultAuthenticateRequestSigningAlgorithm { get { - return (MessageSigningAlgorithm)base[defaultAuthenticateRequestSigningAlgorithm]; + return (string)base[defaultAuthenticateRequestSigningAlgorithm]; } } diff --git a/Kentor.AuthServices/Configuration/SPOptions.cs b/Kentor.AuthServices/Configuration/SPOptions.cs index 727d02b43..8f1c8be85 100644 --- a/Kentor.AuthServices/Configuration/SPOptions.cs +++ b/Kentor.AuthServices/Configuration/SPOptions.cs @@ -357,7 +357,7 @@ private IEnumerable publishableServiceCertificates /// /// The default algorithm to use when signing Authentication Requests /// - public MessageSigningAlgorithm DefaultAuthenticateRequestSigningAlgorithm { get; set; } + public string DefaultAuthenticateRequestSigningAlgorithm { get; set; } /// /// Metadata flag that we want assertions to be signed. /// diff --git a/Kentor.AuthServices/IdentityProvider.cs b/Kentor.AuthServices/IdentityProvider.cs index bf0539bb9..352a327c9 100644 --- a/Kentor.AuthServices/IdentityProvider.cs +++ b/Kentor.AuthServices/IdentityProvider.cs @@ -322,7 +322,7 @@ public string MetadataLocation /// /// Signing Algorithm to be used when signing the Authentication Request /// - public MessageSigningAlgorithm SigningAlgorithm { get; set; } = MessageSigningAlgorithm.RsaSecureHashAlgorithm1; + public string SigningAlgorithm { get; set; } = MessageSigningAlgorithm.RsaSecureHashAlgorithm1; /// /// Bind a Saml2AuthenticateRequest using the active binding of the idp, diff --git a/Kentor.AuthServices/MessageSigningAlgorithm.cs b/Kentor.AuthServices/MessageSigningAlgorithm.cs index 0f0803f3c..ece5fba6b 100644 --- a/Kentor.AuthServices/MessageSigningAlgorithm.cs +++ b/Kentor.AuthServices/MessageSigningAlgorithm.cs @@ -3,24 +3,24 @@ /// /// Enum MessageSigningAlgorithm /// - public enum MessageSigningAlgorithm + public static class MessageSigningAlgorithm { /// /// The rsasha1 /// - RsaSecureHashAlgorithm1, + public const string RsaSecureHashAlgorithm1 = "RsaSecureHashAlgorithm1"; /// /// The rsasha256 /// - RsaSecureHashAlgorithm256, + public const string RsaSecureHashAlgorithm256 = "RsaSecureHashAlgorithm256"; /// /// The rsasha384 /// - RsaSecureHashAlgorithm384, + public const string RsaSecureHashAlgorithm384 = "RsaSecureHashAlgorithm384"; /// /// The rsasha512 /// - RsaSecureHashAlgorithm512 + public const string RsaSecureHashAlgorithm512 = "RsaSecureHashAlgorithm512"; } @@ -32,7 +32,7 @@ public static class MessageSigningDefaults /// /// The default algorithm /// - public const MessageSigningAlgorithm DefaultAlgorithm = + public const string DefaultAlgorithm = MessageSigningAlgorithm.RsaSecureHashAlgorithm1; } } \ No newline at end of file diff --git a/Kentor.AuthServices/SAML2P/ISaml2Message.cs b/Kentor.AuthServices/SAML2P/ISaml2Message.cs index fa8e07abc..69a13070d 100644 --- a/Kentor.AuthServices/SAML2P/ISaml2Message.cs +++ b/Kentor.AuthServices/SAML2P/ISaml2Message.cs @@ -53,7 +53,7 @@ public interface ISaml2Message /// according to the signature processing rules of each binding. /// /// The signing algorithm. - MessageSigningAlgorithm SigningAlgorithm { get; } + string SigningAlgorithm { get; } /// /// Issuer of the message. /// diff --git a/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs b/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs index eeb9d9a72..9cbf2d848 100644 --- a/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs +++ b/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs @@ -168,6 +168,11 @@ private void ValidateCorrectDocument(XmlElement xml) /// to the signature processing rules of each binding. /// public X509Certificate2 SigningCertificate { get; set; } - public MessageSigningAlgorithm SigningAlgorithm { get; set; } + /// + /// The signing algorithm to use when signing the message during binding, + /// according to the signature processing rules of each binding. + /// + /// The signing algorithm. + public string SigningAlgorithm { get; set; } } } diff --git a/Kentor.AuthServices/SAML2P/Saml2Response.cs b/Kentor.AuthServices/SAML2P/Saml2Response.cs index d7547f178..299d9d12c 100644 --- a/Kentor.AuthServices/SAML2P/Saml2Response.cs +++ b/Kentor.AuthServices/SAML2P/Saml2Response.cs @@ -227,8 +227,14 @@ private void ReadAndValidateInResponseTo(XmlElement xml, Saml2Id expectedInRespo /// [ExcludeFromCodeCoverage] public X509Certificate2 SigningCertificate { get; } + + /// + /// The signing algorithm to use when signing the message during binding, + /// according to the signature processing rules of each binding. + /// + /// The signing algorithm. [ExcludeFromCodeCoverage] - public MessageSigningAlgorithm SigningAlgorithm { get; set; } + public string SigningAlgorithm { get; set; } private XmlElement xmlElement; diff --git a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs index 2ff0d3bc5..05d60c973 100644 --- a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs +++ b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs @@ -55,7 +55,12 @@ protected Saml2StatusResponseType(Saml2StatusCode status) /// public X509Certificate2 SigningCertificate { get; set; } - public MessageSigningAlgorithm SigningAlgorithm { get; } + /// + /// The signing algorithm to use when signing the message during binding, + /// according to the signature processing rules of each binding. + /// + /// The signing algorithm. + public string SigningAlgorithm { get; } /// /// Status code of the message. diff --git a/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs b/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs index 758d148bd..d14a515a3 100644 --- a/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs +++ b/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs @@ -48,7 +48,7 @@ public override CommandResult Bind(ISaml2Message message) private static string AddSignature(string queryString, ISaml2Message message) { - string signingAlgorithmUrl = message.SigningAlgorithm.ToNamespace(); + string signingAlgorithmUrl = XmlDocumentSigningExtensions.AlgorithmToXmlDsigNamespace(message.SigningAlgorithm); queryString += "&SigAlg=" + Uri.EscapeDataString(signingAlgorithmUrl); var signatureDescription = (SignatureDescription)CryptoConfig.CreateFromName(signingAlgorithmUrl); diff --git a/Kentor.AuthServices/XmlDocumentSigningExtensions.cs b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs index 6a84c6651..f6774f430 100644 --- a/Kentor.AuthServices/XmlDocumentSigningExtensions.cs +++ b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs @@ -14,7 +14,7 @@ namespace Kentor.AuthServices /// public static class XmlDocumentSigningExtensions { - private static readonly Dictionary AlgorithmToNamespaceMap = new Dictionary + private static readonly Dictionary AlgorithmToNamespaceMap = new Dictionary { { MessageSigningAlgorithm.RsaSecureHashAlgorithm1,RSASHA1}, { MessageSigningAlgorithm.RsaSecureHashAlgorithm256,RSASHA256}, @@ -35,12 +35,21 @@ public static class XmlDocumentSigningExtensions /// /// /// - public static string ToNamespace(this MessageSigningAlgorithm algorithm) + public static string AlgorithmToXmlDsigNamespace(string algorithm) { - return AlgorithmToNamespaceMap[algorithm]; - } + if (string.IsNullOrEmpty(algorithm)) + return AlgorithmToNamespaceMap[MessageSigningDefaults.DefaultAlgorithm]; //logout,etc. - public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, MessageSigningAlgorithm signingAlgorithm) + return AlgorithmToNamespaceMap[algorithm]; + } + + /// + /// Signs the document. + /// + /// The XML document. + /// The signing certificate. + /// The signing algorithm. + public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, string signingAlgorithm) { SignDocument(xmlDocument, signingCertificate, signingAlgorithm, true); } @@ -53,7 +62,7 @@ public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 s /// Whether to include key info clause in the resulting document /// /// - public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, MessageSigningAlgorithm algorithm, bool includeKeyInfo) + public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 signingCertificate, string algorithm, bool includeKeyInfo) { if (xmlDocument == null) { @@ -69,7 +78,7 @@ public static void SignDocument(this XmlDocument xmlDocument, X509Certificate2 s throw new ArgumentNullException(nameof(signingCertificate)); } - string signatureMethodNamespace = algorithm.ToNamespace(); + string signatureMethodNamespace = AlgorithmToXmlDsigNamespace(algorithm); // Note that this will return a Basic cryptoprovider, with only SHA-1 support var key = (RSACryptoServiceProvider) signingCertificate.PrivateKey; From bdff93018d005af244dd0caf45e646e9747eb00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20T=C3=B8nners?= Date: Wed, 14 Dec 2016 16:19:32 +0100 Subject: [PATCH 6/6] Remove unused namespace --- Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs b/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs index 699cb2e0d..afd818faf 100644 --- a/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs +++ b/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs @@ -7,7 +7,6 @@ using System.Xml; using System.Text; using System.Collections.Generic; -using IdentityServer3.Core; using Kentor.AuthServices.Saml2P; using Kentor.AuthServices.WebSso; using Kentor.AuthServices.Tests.WebSSO;