Skip to content

Commit d3ea346

Browse files
authored
Make it easier to work with server and client certificates in the client (#2647)
* Make it easier to work with server and client certificates in the client ### Server certificates Rather then making folks register their ServerCertificateValidation callback globally on the static `ServicePointManager` or subclass `HttpConnection` to set it on the request/itself we now expose it on `ConnectionSettings` this callback is fired for each unique endpoint (node) until it returns true after which its cached for the duration of that servicepoint. We also ship with handy baked in validations on `CertificateValidations`: * `CertificateValidations.AllowAll` simply returns true * `CertificateValidations.DenyAll` simply returns false If your client application however has access to the public CA certificate locally Elasticsearch.NET/NEST ships with handy helpers that assert that the certificate that the server presented was one that came from our local CA certificate. If you use x-pack's `certgen` tool to [generate SSL certificates](https://www.elastic.co/guide/en/x-pack/current/ssl-tls.html) the generated node certificate does not include the CA in the certificate chain. This to cut back on SSL handshake size. In those case you can use `CertificateValidations.AuthorityIsRoot` and pass it your local copy of the CA public key to assert that the certificate the server presented was generated off that. If you go for a vendor generated SSL certificate its common practice for them to include the CA and any intermediary CA's in the certificate chain in those case use `CertificateValidations.AuthorityPartOfChain` which validates that the local CA certificate is part of that chain and was used to generate the servers key. ### Client certificates `ConnectionSettings` now also accepts `ClientCertificates` as a collection or `ClientCertificate` as a single certificate to be used as the user authentication for ALL requests. `RequestConfiguration` accepts the same but will be the client certificate for that single request only. The client certificate should be a certificate that has the public and private key available (`pfx` or `p12`) however x-pack `certgen` generates two separate `cer` and `key` files. For .NET 4.5/4.6 we ship with a helper that creates a proper self contained certificate from these two files `ClientCertificate.LoadWithPrivateKey` but because we can no longer update a certificates `PublicKey` algorithm in .NET core this is not available there. Its typically recommended to generate a single pfx or p12 file since those can just be passed to `X509Certificate`'s constructor * spacing and visibillity changes * try fix mono build of .net 4.* HttpConnection * make sure in unit test mode we skip the certificate tests since they rely on a disk on file, also make sure cluster base does not do the desiredport check when running in unit test mode * only throw when attempting to set callback on mono when callback is not null * callback not in ifdef scope
1 parent a9ed884 commit d3ea346

33 files changed

+1070
-62
lines changed

src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.ComponentModel;
44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Net.Security;
7+
using System.Security.Cryptography.X509Certificates;
68
using System.Threading;
79

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

148+
private Func<object, X509Certificate,X509Chain,SslPolicyErrors, bool> _serverCertificateValidationCallback;
149+
Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> IConnectionConfigurationValues.ServerCertificateValidationCallback => _serverCertificateValidationCallback;
150+
151+
private X509CertificateCollection _clientCertificates;
152+
X509CertificateCollection IConnectionConfigurationValues.ClientCertificates => _clientCertificates;
153+
146154
/// <summary>
147155
/// The default predicate for <see cref="IConnectionPool"/> implementations that return true for <see cref="IConnectionPool.SupportsReseeding"/>
148156
/// in which case master only nodes are excluded from API calls.
@@ -412,6 +420,35 @@ public T EnableDebugMode(Action<IApiCallDetails> onRequestCompleted = null)
412420
return (T)this;
413421
}
414422

423+
/// <summary>
424+
/// Register a ServerCertificateValidationCallback, this is called per endpoint until it returns true.
425+
/// After this callback returns true that endpoint is validated for the lifetime of the ServiceEndpoint
426+
/// for that host.
427+
/// </summary>
428+
public T ServerCertificateValidationCallback(Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> callback) =>
429+
Assign(a => a._serverCertificateValidationCallback = callback);
430+
431+
/// <summary>
432+
/// Use the following certificates to authenticate all HTTP requests. You can also set them on individual
433+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
434+
/// </summary>
435+
public T ClientCertificates(X509CertificateCollection certificates) =>
436+
Assign(a => a._clientCertificates = certificates);
437+
438+
/// <summary>
439+
/// Use the following certificate to authenticate all HTTP requests. You can also set them on individual
440+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
441+
/// </summary>
442+
public T ClientCertificate(X509Certificate certificate) =>
443+
Assign(a => a._clientCertificates = new X509Certificate2Collection { certificate });
444+
445+
/// <summary>
446+
/// Use the following certificate to authenticate all HTTP requests. You can also set them on individual
447+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
448+
/// </summary>
449+
public T ClientCertificate(string certificatePath) =>
450+
Assign(a => a._clientCertificates = new X509Certificate2Collection { new X509Certificate(certificatePath) });
451+
415452
void IDisposable.Dispose() => this.DisposeManagedResources();
416453

417454
protected virtual void DisposeManagedResources()

src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Specialized;
3+
using System.Net.Security;
4+
using System.Security.Cryptography.X509Certificates;
35
using System.Threading;
46

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

177+
/// <summary>
178+
/// Register a ServerCertificateValidationCallback per request
179+
/// </summary>
180+
Func<object, X509Certificate,X509Chain,SslPolicyErrors, bool> ServerCertificateValidationCallback { get; }
181+
175182
/// <summary>
176183
/// 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.
177184
/// When using an <see cref="IConnectionPool"/> implementation that supports reseeding of nodes, this will default to omitting master only node from regular API calls.
178185
/// When using static or single node connection pooling it is assumed the list of node you instantiate the client with should be taken verbatim.
179186
/// </summary>
180187
Func<Node, bool> NodePredicate { get; }
188+
189+
/// <summary>
190+
/// Use the following certificates to authenticate all HTTP requests. You can also set them on individual
191+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
192+
/// </summary>
193+
X509CertificateCollection ClientCertificates { get; }
181194
}
182195
}

src/Elasticsearch.Net/Configuration/RequestConfiguration.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Net.Security;
4+
using System.Security.Cryptography.X509Certificates;
35
using System.Threading;
46

57
namespace Elasticsearch.Net
@@ -74,6 +76,11 @@ public interface IRequestConfiguration
7476
/// <pre/>https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
7577
/// </summary>
7678
string RunAs { get; set; }
79+
80+
/// <summary>
81+
/// Use the following client certificates to authenticate this single request
82+
/// </summary>
83+
X509CertificateCollection ClientCertificates { get; set; }
7784
}
7885

7986
public class RequestConfiguration : IRequestConfiguration
@@ -96,6 +103,8 @@ public class RequestConfiguration : IRequestConfiguration
96103
/// https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
97104
/// </summary>
98105
public string RunAs { get; set; }
106+
107+
public X509CertificateCollection ClientCertificates { get; set; }
99108
}
100109

101110
public class RequestConfigurationDescriptor : IRequestConfiguration
@@ -115,6 +124,7 @@ public class RequestConfigurationDescriptor : IRequestConfiguration
115124
BasicAuthenticationCredentials IRequestConfiguration.BasicAuthenticationCredentials { get; set; }
116125
bool IRequestConfiguration.EnableHttpPipelining { get; set; } = true;
117126
string IRequestConfiguration.RunAs { get; set; }
127+
X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; }
118128

119129
public RequestConfigurationDescriptor(IRequestConfiguration config)
120130
{
@@ -131,6 +141,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
131141
Self.BasicAuthenticationCredentials = config?.BasicAuthenticationCredentials;
132142
Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true;
133143
Self.RunAs = config?.RunAs;
144+
Self.ClientCertificates = config?.ClientCertificates;
134145
}
135146

136147
/// <summary>
@@ -202,6 +213,7 @@ public RequestConfigurationDescriptor ForceNode(Uri uri)
202213
Self.ForceNode = uri;
203214
return this;
204215
}
216+
205217
public RequestConfigurationDescriptor MaxRetries(int retry)
206218
{
207219
Self.MaxRetries = retry;
@@ -222,5 +234,20 @@ public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true)
222234
Self.EnableHttpPipelining = enable;
223235
return this;
224236
}
237+
238+
/// <summary> Use the following client certificates to authenticate this request to Elasticsearch </summary>
239+
public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates)
240+
{
241+
Self.ClientCertificates = certificates;
242+
return this;
243+
}
244+
245+
/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
246+
public RequestConfigurationDescriptor ClientCertificate(X509Certificate certificate) =>
247+
this.ClientCertificates(new X509Certificate2Collection { certificate });
248+
249+
/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
250+
public RequestConfigurationDescriptor ClientCertificate(string certificatePath) =>
251+
this.ClientCertificates(new X509Certificate2Collection {new X509Certificate(certificatePath)});
225252
}
226253
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Net;
4+
using System.Net.Security;
5+
using System.Security.Cryptography.X509Certificates;
6+
7+
namespace Elasticsearch.Net
8+
{
9+
/// <summary>
10+
/// A collection of handy baked in server certificate validation callbacks
11+
/// </summary>
12+
public static class CertificateValidations
13+
{
14+
/// <summary>
15+
/// DANGEROUS, never use this in production validates ALL certificates to true.
16+
/// </summary>
17+
/// <returns>Always true, allowing ALL certificates</returns>
18+
public static bool AllowAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => true;
19+
20+
/// <summary>
21+
/// Always false, in effect blocking ALL certificates
22+
/// </summary>
23+
/// <returns>Always false, always blocking ALL certificates</returns>
24+
public static bool DenyAll(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) => false;
25+
26+
/// <summary>
27+
/// Helper to create a certificate validation callback based on the certificate authority certificate that we used to
28+
/// generate the nodes certificates with. This callback expects the CA to be part of the chain as intermediate CA.
29+
/// </summary>
30+
/// <param name="caCertificate">The ca certificate used to generate the nodes certificate </param>
31+
/// <param name="trustRoot">Custom CA are never trusted by default unless they are in the machines trusted store, set this to true
32+
/// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted.
33+
/// </param>
34+
/// <param name="revocationMode">By default we do not check revocation, it is however recommended to check this (either offline or online).</param>
35+
public static Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> AuthorityPartOfChain(
36+
X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck) =>
37+
(sender, cert, chain, errors) =>
38+
errors == SslPolicyErrors.None
39+
|| ValidIntermediateCa(caCertificate, cert, chain, trustRoot, revocationMode);
40+
41+
/// <summary>
42+
/// Helper to create a certificate validation callback based on the certificate authority certificate that we used to
43+
/// generate the nodes certificates with. This callback does NOT expect the CA to be part of the chain presented by the server.
44+
/// Including the root certificate in the chain increases the SSL handshake size and Elasticsearch's certgen by default does not include
45+
/// the CA in the certificate chain.
46+
/// </summary>
47+
/// <param name="caCertificate">The ca certificate used to generate the nodes certificate </param>
48+
/// <param name="trustRoot">Custom CA are never trusted by default unless they are in the machines trusted store, set this to true
49+
/// if you've added the CA to the machines trusted store. In which case UntrustedRoot should not be accepted.
50+
/// </param>
51+
/// <param name="revocationMode">By default we do not check revocation, it is however recommended to check this (either offline or online).</param>
52+
public static Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> AuthorityIsRoot(
53+
X509Certificate caCertificate, bool trustRoot = true, X509RevocationMode revocationMode = X509RevocationMode.NoCheck) =>
54+
(sender, cert, chain, errors) =>
55+
errors == SslPolicyErrors.None
56+
|| ValidRootCa(caCertificate, cert, chain, trustRoot, revocationMode);
57+
58+
private static X509Certificate2 to2(X509Certificate certificate)
59+
{
60+
#if DOTNETCORE
61+
return new X509Certificate2(certificate.Export(X509ContentType.Cert));
62+
#else
63+
return new X509Certificate2(certificate);
64+
#endif
65+
}
66+
67+
private static bool ValidRootCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot,
68+
X509RevocationMode revocationMode)
69+
{
70+
var ca = to2(caCertificate);
71+
var privateChain = new X509Chain {ChainPolicy = {RevocationMode = revocationMode}};
72+
privateChain.ChainPolicy.ExtraStore.Add(ca);
73+
privateChain.Build(to2(certificate));
74+
75+
//lets validate the our chain status
76+
foreach (var chainStatus in privateChain.ChainStatus)
77+
{
78+
//custom CA's that are not in the machine trusted store will always have this status
79+
//by setting trustRoot = true (default) we skip this error
80+
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue;
81+
//trustRoot is false so we expected our CA to be in the machines trusted store
82+
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false;
83+
//otherwise if the chain has any error of any sort return false
84+
if (chainStatus.Status != X509ChainStatusFlags.NoError) return false;
85+
}
86+
return true;
87+
88+
}
89+
90+
private static bool ValidIntermediateCa(X509Certificate caCertificate, X509Certificate certificate, X509Chain chain, bool trustRoot,
91+
X509RevocationMode revocationMode)
92+
{
93+
var ca = to2(caCertificate);
94+
var privateChain = new X509Chain {ChainPolicy = {RevocationMode = revocationMode}};
95+
privateChain.ChainPolicy.ExtraStore.Add(ca);
96+
privateChain.Build(to2(certificate));
97+
98+
//Assert our chain has the same number of elements as the certifcate presented by the server
99+
if (chain.ChainElements.Count != privateChain.ChainElements.Count) return false;
100+
101+
//lets validate the our chain status
102+
foreach (var chainStatus in privateChain.ChainStatus)
103+
{
104+
//custom CA's that are not in the machine trusted store will always have this status
105+
//by setting trustRoot = true (default) we skip this error
106+
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot && trustRoot) continue;
107+
//trustRoot is false so we expected our CA to be in the machines trusted store
108+
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot) return false;
109+
//otherwise if the chain has any error of any sort return false
110+
if (chainStatus.Status != X509ChainStatusFlags.NoError) return false;
111+
}
112+
113+
var found = false;
114+
//We are going to walk both chains and make sure the thumbprints align
115+
//while making sure one of the chains certificates presented by the server has our expected CA thumbprint
116+
for (var i = 0; i < chain.ChainElements.Count; i++)
117+
{
118+
var c = chain.ChainElements[i].Certificate.Thumbprint;
119+
var cPrivate = privateChain.ChainElements[i].Certificate.Thumbprint;
120+
if (c == ca.Thumbprint) found = true;
121+
122+
//mis aligned certificate chain, return false so we do not accept this certificate
123+
if (c != cPrivate) return false;
124+
i++;
125+
}
126+
return found;
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)