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/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/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("
opt .Excluding(s => s.XmlElement) .Excluding(s => s.SigningCertificate) + .Excluding(s => s.SigningAlgorithm) .Excluding(s => s.SessionNotOnOrAfter)); } diff --git a/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs b/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs index e7e57af92..f3847337e 100644 --- a/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs +++ b/Kentor.AuthServices.Tests/WebSSO/Saml2MessageImplementation.cs @@ -25,6 +25,7 @@ public string ToXml() public string XmlData { get; set; } public X509Certificate2 SigningCertificate { get; set; } + 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..afd818faf 100644 --- a/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs +++ b/Kentor.AuthServices.Tests/WebSSO/Saml2PostBindingTests.cs @@ -1,104 +1,105 @@ -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 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 +123,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 +169,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 +220,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.Tests/XmlHelpersTests.cs b/Kentor.AuthServices.Tests/XmlHelpersTests.cs index 7dec78173..70eab290f 100644 --- a/Kentor.AuthServices.Tests/XmlHelpersTests.cs +++ b/Kentor.AuthServices.Tests/XmlHelpersTests.cs @@ -1,373 +1,407 @@ -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; - -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 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..25670fe5b 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 string AuthenticateRequestSigningAlgorithm + { + get + { + return (string)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..3e903deaf 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 string DefaultAuthenticateRequestSigningAlgorithm + { + get + { + return (string)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 9cb8501f8..f6511320f 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); @@ -353,7 +355,10 @@ private IEnumerable publishableServiceCertificates /// Signing behaviour for AuthnRequests. /// public SigningBehavior AuthenticateRequestSigningBehavior { get; set; } - + /// + /// The default algorithm to use when signing Authentication Requests + /// + 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 ccb9a5c4f..352a327c9 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; @@ -290,7 +296,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 = this.SigningAlgorithm }; if (spOptions.AuthenticateRequestSigningBehavior == SigningBehavior.Always @@ -312,6 +319,11 @@ public string MetadataLocation return authnRequest; } + /// + /// Signing Algorithm to be used when signing the Authentication Request + /// + public string SigningAlgorithm { get; set; } = MessageSigningAlgorithm.RsaSecureHashAlgorithm1; + /// /// Bind a Saml2AuthenticateRequest using the active binding of the idp, /// producing a CommandResult with the result of the binding. @@ -501,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/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..ece5fba6b --- /dev/null +++ b/Kentor.AuthServices/MessageSigningAlgorithm.cs @@ -0,0 +1,38 @@ +namespace Kentor.AuthServices.Saml2P +{ + /// + /// Enum MessageSigningAlgorithm + /// + public static class MessageSigningAlgorithm + { + /// + /// The rsasha1 + /// + public const string RsaSecureHashAlgorithm1 = "RsaSecureHashAlgorithm1"; + /// + /// The rsasha256 + /// + public const string RsaSecureHashAlgorithm256 = "RsaSecureHashAlgorithm256"; + /// + /// The rsasha384 + /// + public const string RsaSecureHashAlgorithm384 = "RsaSecureHashAlgorithm384"; + /// + /// The rsasha512 + /// + public const string RsaSecureHashAlgorithm512 = "RsaSecureHashAlgorithm512"; + + } + + /// + /// Class MessageSigningDefaults. + /// + public static class MessageSigningDefaults + { + /// + /// The default algorithm + /// + 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 8293fcc1b..69a13070d 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. + string SigningAlgorithm { get; } /// /// Issuer of the message. /// diff --git a/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs b/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs index 3414288e3..9cbf2d848 100644 --- a/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs +++ b/Kentor.AuthServices/SAML2P/Saml2RequestBase.cs @@ -168,5 +168,11 @@ private void ValidateCorrectDocument(XmlElement xml) /// to the signature processing rules of each binding. /// public X509Certificate2 SigningCertificate { 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 2e9c7a6a9..299d9d12c 100644 --- a/Kentor.AuthServices/SAML2P/Saml2Response.cs +++ b/Kentor.AuthServices/SAML2P/Saml2Response.cs @@ -228,6 +228,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 string SigningAlgorithm { get; set; } + private XmlElement xmlElement; /// diff --git a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs index 59bd40d6f..05d60c973 100644 --- a/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs +++ b/Kentor.AuthServices/SAML2P/Saml2StatusResponseType.cs @@ -55,6 +55,13 @@ protected Saml2StatusResponseType(Saml2StatusCode status) /// public X509Certificate2 SigningCertificate { 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; } + /// /// Status code of the message. /// diff --git a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs index 10315fcc6..d89be5e1b 100644 --- a/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs +++ b/Kentor.AuthServices/WebSSO/Saml2PostBinding.cs @@ -65,24 +65,33 @@ 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 + var relayStateHtml = string.IsNullOrEmpty(message.RelayState) ? null : string.Format(CultureInfo.InvariantCulture, PostHtmlRelayStateFormatString, message.RelayState); var cr = new CommandResult() { - ContentType = "text/html", + ContentType = "text/html", Content = String.Format( - CultureInfo.InvariantCulture, - PostHtmlFormatString, - message.DestinationUrl, - relayStateHtml, - message.MessageName, + CultureInfo.InvariantCulture, + PostHtmlFormatString, + message.DestinationUrl, + relayStateHtml, + message.MessageName, encodedXml) }; diff --git a/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs b/Kentor.AuthServices/WebSSO/Saml2RedirectBinding.cs index e906f52ab..d14a515a3 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 = XmlDocumentSigningExtensions.AlgorithmToXmlDsigNamespace(message.SigningAlgorithm); - 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..f6774f430 --- /dev/null +++ b/Kentor.AuthServices/XmlDocumentSigningExtensions.cs @@ -0,0 +1,113 @@ +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 AlgorithmToXmlDsigNamespace(string algorithm) + { + if (string.IsNullOrEmpty(algorithm)) + return AlgorithmToNamespaceMap[MessageSigningDefaults.DefaultAlgorithm]; //logout,etc. + + 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); + } + /// + /// 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, string algorithm, bool includeKeyInfo) + { + 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 = AlgorithmToXmlDsigNamespace(algorithm); + + // Note that this will return a Basic cryptoprovider, 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) {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); + + if (includeKeyInfo) + { + var keyInfo = new KeyInfo(); + keyInfo.AddClause(new KeyInfoX509Data(signingCertificate)); + signedXml.KeyInfo = keyInfo; + } + + signedXml.ComputeSignature(); + xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(signedXml.GetXml(), true)); + } + } + } + } +} \ No newline at end of file