Skip to content

Commit

Permalink
Client certificates for artifact resolve
Browse files Browse the repository at this point in the history
- Fixes Sustainsys#367
- Closes Sustainsys#716
- Closes Sustainsys#789
  • Loading branch information
AndersAbel committed Nov 8, 2018
2 parents 7000a47 + adcc285 commit 6c3f2e1
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 24 deletions.
16 changes: 13 additions & 3 deletions Sustainsys.Saml2/CertificateUse.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
namespace Sustainsys.Saml2
using System;

namespace Sustainsys.Saml2
{
/// <summary>
/// How is the certificate used?
/// </summary>
[Flags]
public enum CertificateUse
{
/// <summary>
/// The certificate is used for either signing or encryption, or both
/// The certificate is used for either signing or encryption, or both.
/// Equivalent to Signing | Encryption.
/// </summary>
Both = 0,

Expand All @@ -18,6 +22,12 @@ public enum CertificateUse
/// <summary>
/// The certificate is used for decrypting inbound assertions
/// </summary>
Encryption = 2
Encryption = 2,

/// <summary>
/// The certificate is used as a Tls Client certificate for outbound
/// tls requests.
/// </summary>
TlsClient = 4
}
}
40 changes: 26 additions & 14 deletions Sustainsys.Saml2/Configuration/SPOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public ReadOnlyCollection<X509Certificate2> DecryptionServiceCertificates
get
{
var decryptionCertificates = ServiceCertificates
.Where(c => c.Use == CertificateUse.Encryption || c.Use == CertificateUse.Both)
.Where(c => c.Use.HasFlag(CertificateUse.Encryption) || c.Use == CertificateUse.Both)
.Select(c => c.Certificate);

return decryptionCertificates.ToList().AsReadOnly();
Expand All @@ -262,7 +262,7 @@ public X509Certificate2 SigningServiceCertificate
{
var signingCertificates = ServiceCertificates
.Where(c => c.Status == CertificateStatus.Current)
.Where(c => c.Use == CertificateUse.Signing || c.Use == CertificateUse.Both)
.Where(c => c.Use.HasFlag(CertificateUse.Signing) || c.Use == CertificateUse.Both)
.Select(c => c.Certificate);

return signingCertificates.FirstOrDefault();
Expand All @@ -276,25 +276,18 @@ public ReadOnlyCollection<ServiceCertificate> MetadataCertificates
{
get
{
var futureEncryptionCertExists = publishableServiceCertificates
var futureEncryptionCertExists = PublishableServiceCertificates
.Any(c => c.Status == CertificateStatus.Future && (c.Use == CertificateUse.Encryption || c.Use == CertificateUse.Both));

var metaDataCertificates = publishableServiceCertificates
var metaDataCertificates = PublishableServiceCertificates
.Where(
// Signing & "Both" certs always get published because we want Idp's to be aware of upcoming keys
c => c.Status == CertificateStatus.Future || c.Use != CertificateUse.Encryption
// But current Encryption cert stops getting published immediately when a Future one is added
// (of course we still decrypt with the current cert, but that's a different part of the code)
|| (c.Status == CertificateStatus.Current && c.Use == CertificateUse.Encryption && !futureEncryptionCertExists)
|| c.MetadataPublishOverride != MetadataPublishOverrideType.None
)
.Select(c => new ServiceCertificate
{
Use = c.Use,
Status = c.Status,
MetadataPublishOverride = c.MetadataPublishOverride,
Certificate = c.Certificate
}).ToList();
).ToList();

var futureBothCertExists = metaDataCertificates
.Any(c => c.Status == CertificateStatus.Future && c.Use == CertificateUse.Both);
Expand Down Expand Up @@ -327,12 +320,31 @@ public ReadOnlyCollection<ServiceCertificate> MetadataCertificates
}
}

private IEnumerable<ServiceCertificate> publishableServiceCertificates
private static CertificateUse ConvertUse(CertificateUse certificateUse)
{
var use = certificateUse & (CertificateUse.Signing | CertificateUse.Encryption);

if (use == (CertificateUse.Signing | CertificateUse.Encryption))
{
use = CertificateUse.Both;
}
return use;
}

private IEnumerable<ServiceCertificate> PublishableServiceCertificates
{
get
{
return ServiceCertificates
.Where(c => c.MetadataPublishOverride != MetadataPublishOverrideType.DoNotPublish);
.Where(c => c.MetadataPublishOverride != MetadataPublishOverrideType.DoNotPublish
&& c.Use != CertificateUse.TlsClient) // Certs that are only Tls should not be published.
.Select(c => new ServiceCertificate // Finally create new instances and convert use to ignore Tls.
{
Use = ConvertUse(c.Use),
Status = c.Status,
MetadataPublishOverride = c.MetadataPublishOverride,
Certificate = c.Certificate
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
namespace Sustainsys.Saml2.Configuration
{
/// <summary>
/// Certificates used by the service provider for signing or decryption.
/// Certificates used by the service provider for signing, decryption and
/// TLS client certificates for artifact resolve.
/// </summary>
public class ServiceCertificateCollection: Collection<ServiceCertificate>
{
Expand All @@ -21,7 +22,7 @@ public class ServiceCertificateCollection: Collection<ServiceCertificate>
public void Add(X509Certificate2 certificate)
{
if (certificate == null) throw new ArgumentNullException(nameof(certificate));
InsertItem(base.Count, new ServiceCertificate
InsertItem(Count, new ServiceCertificate
{
Certificate = certificate
});
Expand Down
43 changes: 43 additions & 0 deletions Sustainsys.Saml2/Internal/ClientCertificateWebClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Sustainsys.Saml2.Internal
{
/// <summary>
/// A WebClient implementation that will add a list of client
/// certificates to the requests it makes.
/// </summary>
internal class ClientCertificateWebClient : WebClient
{
private readonly IEnumerable<X509Certificate2> certificates;
/// <summary>
/// Register the certificate to be used for this requets.
/// </summary>
/// <param name="certificates">Certificates to offer to server</param>
public ClientCertificateWebClient(IEnumerable<X509Certificate2> certificates)
{
this.certificates = certificates;
}
/// <summary>
/// Override the base class to add the certificate
/// to the reuqest before returning it.
/// </summary>
/// <param name="address"></param>
/// <returns></returns>
protected override WebRequest GetWebRequest(Uri address)
{
var request = (HttpWebRequest)base.GetWebRequest(address);
if (certificates != null)
{
foreach(var c in certificates)
{
request.ClientCertificates.Add(c);
}
}
return request;
}
}
}
21 changes: 19 additions & 2 deletions Sustainsys.Saml2/SAML2P/Saml2SoapBinding.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using Sustainsys.Saml2.Internal;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
Expand Down Expand Up @@ -53,6 +55,21 @@ public static XmlElement ExtractBody(string xml)
/// <param name="destination">Destination endpoint</param>
/// <returns>Response.</returns>
public static XmlElement SendSoapRequest(string payload, Uri destination)
{
return SendSoapRequest(payload, destination, null);
}

/// <summary>
/// Send a SOAP request to the specified endpoint and return the result.
/// </summary>
/// <param name="payload">Message payload</param>
/// <param name="destination">Destination endpoint</param>
/// <param name="clientCertificates">Client certificates to offer to the server.</param>
/// <returns>Response.</returns>
public static XmlElement SendSoapRequest(
string payload,
Uri destination,
IEnumerable<X509Certificate2> clientCertificates)
{
if(destination == null)
{
Expand All @@ -71,7 +88,7 @@ public static XmlElement SendSoapRequest(string payload, Uri destination)

var message = CreateSoapBody(payload);

using (var client = new WebClient())
using (var client = new ClientCertificateWebClient(clientCertificates))
{
client.Headers.Add("SOAPAction", "http://www.oasis-open.org/committees/security");
var response = client.UploadString(destination, message);
Expand Down
6 changes: 5 additions & 1 deletion Sustainsys.Saml2/WebSSO/Saml2ArtifactBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ private static XmlElement ResolveArtifact(

options.SPOptions.Logger.WriteVerbose("Calling idp " + idp.EntityId.Id + " to resolve artifact\n" + artifact);

var response = Saml2SoapBinding.SendSoapRequest(payload, arsUri);
var clientCertificates = options.SPOptions.ServiceCertificates
.Where(sc => sc.Use.HasFlag(CertificateUse.TlsClient) && sc.Status == CertificateStatus.Current)
.Select(sc => sc.Certificate);

var response = Saml2SoapBinding.SendSoapRequest(payload, arsUri, clientCertificates);

options.SPOptions.Logger.WriteVerbose("Artifact resolved returned\n" + response);

Expand Down
2 changes: 1 addition & 1 deletion Tests/Tests.NETCore/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</requestedAttributes>
</metadata>
<serviceCertificates>
<add fileName="Sustainsys.Saml2.Tests2.pfx"/>
<add fileName="Sustainsys.Saml2.Tests2.pfx" use="Signing, Encryption, TlsClient"/>
</serviceCertificates>
<identityProviders>
<add entityId="https://idp.example.com" signOnUrl="https://idp.example.com/idp" logoutUrl="https://idp.example.com/logout" allowUnsolicitedAuthnResponse="true" binding="HttpRedirect">
Expand Down
2 changes: 1 addition & 1 deletion Tests/Tests.NETFramework/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</requestedAttributes>
</metadata>
<serviceCertificates>
<add fileName="Sustainsys.Saml2.Tests2.pfx" />
<add fileName="Sustainsys.Saml2.Tests2.pfx" use="Signing, Encryption, TlsClient"/>
</serviceCertificates>
<identityProviders>
<add entityId="https://idp.example.com" signOnUrl="https://idp.example.com/idp" logoutUrl="https://idp.example.com/logout" allowUnsolicitedAuthnResponse="true" binding="HttpRedirect">
Expand Down
70 changes: 70 additions & 0 deletions Tests/Tests.Shared/Configuration/SPOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,29 @@ public void SPOptions_MetadataCertificates_OnlyOneEncryptionPublished_FutureBoth
var result = subject.MetadataCertificates;
result.Count.Should().Be(1);
result[0].Status.Should().Be(CertificateStatus.Future);
result[0].Use.Should().Be(CertificateUse.Both);
}

[TestMethod]
public void SPOptions_MetadataCertificates_FutureTlsClientCertAndEncryptionDetectedAsFutureEncryption()
{
var subject = new SPOptions();
subject.ServiceCertificates.Add(new ServiceCertificate
{
Use = CertificateUse.Encryption,
Certificate = SignedXmlHelper.TestCert
});
subject.ServiceCertificates.Add(new ServiceCertificate
{
Use = CertificateUse.Encryption | CertificateUse.Signing | CertificateUse.TlsClient,
Status = CertificateStatus.Future,
Certificate = SignedXmlHelper.TestCert2
});

var result = subject.MetadataCertificates;
result.Count.Should().Be(1);
result[0].Status.Should().Be(CertificateStatus.Future);
result[0].Use.Should().Be(CertificateUse.Both);
}

[TestMethod]
Expand Down Expand Up @@ -549,6 +572,53 @@ public void SPOptions_MetadataCertificates_CurrentEncryptionRemainsPublished_IfF
result[0].Status.Should().Be(CertificateStatus.Current);
}

[TestMethod]
public void SPOptions_MetadataCertificates_TlsClientCertIgnored()
{
var subject = new SPOptions();

subject.ServiceCertificates.Add(new ServiceCertificate
{
Use = CertificateUse.TlsClient,
Certificate = SignedXmlHelper.TestCert
});

var result = subject.MetadataCertificates;
result.Count.Should().Be(0);
}

[TestMethod]
public void SPOptions_MetadataCertificates_TlsAndSigningHandled()
{
var subject = new SPOptions();

subject.ServiceCertificates.Add(new ServiceCertificate
{
Use = CertificateUse.TlsClient | CertificateUse.Signing,
Certificate = SignedXmlHelper.TestCert
});

var result = subject.MetadataCertificates;
result.Count.Should().Be(1);
result[0].Use.Should().Be(CertificateUse.Signing);
}

[TestMethod]
public void SPOptions_MetadataCertificates_AllFlagsBecomesBoth()
{
var subject = new SPOptions();

subject.ServiceCertificates.Add(new ServiceCertificate
{
Use = CertificateUse.TlsClient | CertificateUse.Signing | CertificateUse.Encryption,
Certificate = SignedXmlHelper.TestCert
});

var result = subject.MetadataCertificates;
result.Count.Should().Be(1);
result[0].Use.Should().Be(CertificateUse.Both);
}

[TestMethod]
public void SPOptions_Saml2PSecurityTokenHandler_Setter()
{
Expand Down
Loading

0 comments on commit 6c3f2e1

Please sign in to comment.