Skip to content

Make it easier to work with server and client certificates in the client #2647

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

#if DOTNETCORE
Expand Down Expand Up @@ -143,6 +145,12 @@ private static void DefaultRequestDataCreated(RequestData response) { }
private Action<RequestData> _onRequestDataCreated = DefaultRequestDataCreated;
Action<RequestData> IConnectionConfigurationValues.OnRequestDataCreated => _onRequestDataCreated;

private Func<object, X509Certificate,X509Chain,SslPolicyErrors, bool> _serverCertificateValidationCallback;
Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> IConnectionConfigurationValues.ServerCertificateValidationCallback => _serverCertificateValidationCallback;

private X509CertificateCollection _clientCertificates;
X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates;

/// <summary>
/// The default predicate for <see cref="IConnectionPool"/> implementations that return true for <see cref="IConnectionPool.SupportsReseeding"/>
/// in which case master only nodes are excluded from API calls.
Expand Down Expand Up @@ -412,6 +420,35 @@ public T EnableDebugMode(Action<IApiCallDetails> onRequestCompleted = null)
return (T)this;
}

/// <summary>
/// Register a ServerCertificateValidationCallback, this is called per endpoint until it returns true.
/// After this callback returns true that endpoint is validated for the lifetime of the ServiceEndpoint
/// for that host.
/// </summary>
public T ServerCertificateValidationCallback(Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> callback) =>
Assign(a => a._serverCertificateValidationCallback = callback);

/// <summary>
/// Use the following certificates to authenticate all HTTP requests. You can also set them on individual
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
/// </summary>
public T ClientCertificates(X509CertificateCollection certificates) =>
Assign(a => a._clientCertificates = certificates);

/// <summary>
/// Use the following certificate to authenticate all HTTP requests. You can also set them on individual
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
/// </summary>
public T ClientCertificate(X509Certificate certificate) =>
Assign(a => a._clientCertificates = new X509Certificate2Collection { certificate });

/// <summary>
/// Use the following certificate to authenticate all HTTP requests. You can also set them on individual
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
/// </summary>
public T ClientCertificate(string certificatePath) =>
Assign(a => a._clientCertificates = new X509Certificate2Collection { new X509Certificate(certificatePath) });

void IDisposable.Dispose() => this.DisposeManagedResources();

protected virtual void DisposeManagedResources()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Specialized;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace Elasticsearch.Net
Expand Down Expand Up @@ -172,11 +174,22 @@ public interface IConnectionConfigurationValues : IDisposable
/// </summary>
TimeSpan? KeepAliveInterval { get; }

/// <summary>
/// Register a ServerCertificateValidationCallback per request
/// </summary>
Func<object, X509Certificate,X509Chain,SslPolicyErrors, bool> ServerCertificateValidationCallback { get; }

/// <summary>
/// Register a predicate to select which nodes that you want to execute API calls on. Note that sniffing requests omit this predicate and always execute on all nodes.
/// When using an <see cref="IConnectionPool"/> implementation that supports reseeding of nodes, this will default to omitting master only node from regular API calls.
/// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken verbatim.
/// </summary>
Func<Node, bool> NodePredicate { get; }

/// <summary>
/// Use the following certificates to authenticate all HTTP requests. You can also set them on individual
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
/// </summary>
X509CertificateCollection ClientCertificates { get; }
}
}
27 changes: 27 additions & 0 deletions src/Elasticsearch.Net/Configuration/RequestConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace Elasticsearch.Net
Expand Down Expand Up @@ -74,6 +76,11 @@ public interface IRequestConfiguration
/// <pre/>https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
/// </summary>
string RunAs { get; set; }

/// <summary>
/// Use the following client certificates to authenticate this single request
/// </summary>
X509CertificateCollection ClientCertificates { get; set; }
}

public class RequestConfiguration : IRequestConfiguration
Expand All @@ -96,6 +103,8 @@ public class RequestConfiguration : IRequestConfiguration
/// https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
/// </summary>
public string RunAs { get; set; }

public X509CertificateCollection ClientCertificates { get; set; }
}

public class RequestConfigurationDescriptor : IRequestConfiguration
Expand All @@ -115,6 +124,7 @@ public class RequestConfigurationDescriptor : IRequestConfiguration
BasicAuthenticationCredentials IRequestConfiguration.BasicAuthenticationCredentials { get; set; }
bool IRequestConfiguration.EnableHttpPipelining { get; set; } = true;
string IRequestConfiguration.RunAs { get; set; }
X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; }

public RequestConfigurationDescriptor(IRequestConfiguration config)
{
Expand All @@ -131,6 +141,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
Self.BasicAuthenticationCredentials = config?.BasicAuthenticationCredentials;
Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true;
Self.RunAs = config?.RunAs;
Self.ClientCertificates = config?.ClientCertificates;
}

/// <summary>
Expand Down Expand Up @@ -202,6 +213,7 @@ public RequestConfigurationDescriptor ForceNode(Uri uri)
Self.ForceNode = uri;
return this;
}

public RequestConfigurationDescriptor MaxRetries(int retry)
{
Self.MaxRetries = retry;
Expand All @@ -222,5 +234,20 @@ public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true)
Self.EnableHttpPipelining = enable;
return this;
}

/// <summary> Use the following client certificates to authenticate this request to Elasticsearch </summary>
public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates)
{
Self.ClientCertificates = certificates;
return this;
}

/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
public RequestConfigurationDescriptor ClientCertificate(X509Certificate certificate) =>
this.ClientCertificates(new X509Certificate2Collection { certificate });

/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
public RequestConfigurationDescriptor ClientCertificate(string certificatePath) =>
this.ClientCertificates(new X509Certificate2Collection {new X509Certificate(certificatePath)});
}
}
129 changes: 129 additions & 0 deletions src/Elasticsearch.Net/Connection/CertificateValidations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace Elasticsearch.Net
{
/// <summary>
/// A collection of handy baked in server certificate validation callbacks
/// </summary>
public static class CertificateValidations
{
/// <summary>
/// DANGEROUS, never use this in production validates ALL certificates to true.
/// </summary>
/// <returns>Always true, allowing ALL certificates</returns>
public static bool AllowAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => true;

/// <summary>
/// Always false, in effect blocking ALL certificates
/// </summary>
/// <returns>Always false, always blocking ALL certificates</returns>
public static bool DenyAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => false;

/// <summary>
/// Helper to create a certificate validation callback based on the certificate authority certificate that we used to
/// generate the nodes certificates with. This callback expects the CA to be part of the chain as intermediate CA.
/// </summary>
/// <param name="caCertificate">The ca certificate used to generate the nodes certificate </param>
/// <param name="trustRoot">Custom CA are never trusted by default unless they are in the machines trusted store, set this to true
/// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted.
/// </param>
/// <param name="revocationMode">By default we do not check revocation, it is however recommended to check this (either offline or online).</param>
public static Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> AuthorityPartOfChain(
X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck) =>
(sender, cert, chain, errors) =>
errors == SslPolicyErrors.None
|| ValidIntermediateCa(caCertificate, cert, chain, trustRoot, revocationMode);

/// <summary>
/// Helper to create a certificate validation callback based on the certificate authority certificate that we used to
/// generate the nodes certificates with. This callback does NOT expect the CA to be part of the chain presented by the server.
/// Including the root certificate in the chain increases the SSL handshake size and Elasticsearch's certgen by default does not include
/// the CA in the certificate chain.
/// </summary>
/// <param name="caCertificate">The ca certificate used to generate the nodes certificate </param>
/// <param name="trustRoot">Custom CA are never trusted by default unless they are in the machines trusted store, set this to true
/// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted.
/// </param>
/// <param name="revocationMode">By default we do not check revocation, it is however recommended to check this (either offline or online).</param>
public static Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> AuthorityIsRoot(
X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck) =>
(sender, cert, chain, errors) =>
errors == SslPolicyErrors.None
|| ValidRootCa(caCertificate, cert, chain, trustRoot, revocationMode);

private static X509Certificate2 to2(X509Certificate certificate)
{
#if DOTNETCORE
return new X509Certificate2(certificate.Export(X509ContentType.Cert));
#else
return new X509Certificate2(certificate);
#endif
}

private static bool ValidRootCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot,
X509RevocationMode revocationMode)
{
var ca = to2(caCertificate);
var privateChain = new X509Chain {ChainPolicy = {RevocationMode = revocationMode}};
privateChain.ChainPolicy.ExtraStore.Add(ca);
privateChain.Build(to2(certificate));

//lets validate the our chain status
foreach (var chainStatus in privateChain.ChainStatus)
{
//custom CA's that are not in the machine trusted store will always have this status
//by setting trustRoot = true (default) we skip this error
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue;
//trustRoot is false so we expected our CA to be in the machines trusted store
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false;
//otherwise if the chain has any error of any sort return false
if (chainStatus.Status != X509ChainStatusFlags.NoError) return false;
}
return true;

}

private static bool ValidIntermediateCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot,
X509RevocationMode revocationMode)
{
var ca = to2(caCertificate);
var privateChain = new X509Chain {ChainPolicy = {RevocationMode = revocationMode}};
privateChain.ChainPolicy.ExtraStore.Add(ca);
privateChain.Build(to2(certificate));

//Assert our chain has the same number of elements as the certifcate presented by the server
if (chain.ChainElements.Count != privateChain.ChainElements.Count) return false;

//lets validate the our chain status
foreach (var chainStatus in privateChain.ChainStatus)
{
//custom CA's that are not in the machine trusted store will always have this status
//by setting trustRoot = true (default) we skip this error
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue;
//trustRoot is false so we expected our CA to be in the machines trusted store
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false;
//otherwise if the chain has any error of any sort return false
if (chainStatus.Status != X509ChainStatusFlags.NoError) return false;
}

var found = false;
//We are going to walk both chains and make sure the thumbprints align
//while making sure one of the chains certificates presented by the server has our expected CA thumbprint
for (var i = 0; i < chain.ChainElements.Count; i++)
{
var c = chain.ChainElements[i].Certificate.Thumbprint;
var cPrivate = privateChain.ChainElements[i].Certificate.Thumbprint;
if (c == ca.Thumbprint) found = true;

//mis aligned certificate chain, return false so we do not accept this certificate
if (c != cPrivate) return false;
i++;
}
return found;
}
}
}
Loading