Skip to content

Commit 2e9bcf8

Browse files
committed
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 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. `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 Conflicts: src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs src/Elasticsearch.Net/Configuration/RequestConfiguration.cs src/Elasticsearch.Net/Transport/Pipeline/RequestData.cs src/Tests/Framework/ManagedElasticsearch/Nodes/ElasticsearchNode.cs src/Tests/Tests.csproj
1 parent f441e00 commit 2e9bcf8

32 files changed

+751
-74
lines changed

docs/client-concepts/high-level/mapping/auto-map.asciidoc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,51 @@ var expected = new
698698
Expect(expected).WhenSerializing((ICreateIndexRequest)descriptor);
699699
----
700700

701+
[source,csharp]
702+
----
703+
public class ParentWithStringId : Parent
704+
{
705+
public new string Id { get; set; }
706+
}
707+
----
708+
709+
[source,csharp]
710+
----
711+
var descriptor = new CreateIndexDescriptor("myindex")
712+
.Mappings(ms => ms
713+
.Map<ParentWithStringId>(m => m
714+
.AutoMap()
715+
)
716+
);
717+
718+
var expected = new
719+
{
720+
mappings = new
721+
{
722+
parent = new
723+
{
724+
properties = new
725+
{
726+
id = new
727+
{
728+
type = "string",
729+
}
730+
}
731+
}
732+
}
733+
};
734+
735+
var settings = WithConnectionSettings(s => s
736+
.InferMappingFor<ParentWithStringId>(m => m
737+
.TypeName("parent")
738+
.Ignore(p => p.Description)
739+
.Ignore(p => p.IgnoreMe)
740+
)
741+
);
742+
743+
settings.Expect(expected).WhenSerializing((ICreateIndexRequest) descriptor);
744+
----
745+
701746
[[ignoring-properties]]
702747
[float]
703748
== Ignoring Properties

src/Elasticsearch.Net/Configuration/ConnectionConfiguration.cs

Lines changed: 36 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,11 @@ 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;
146153
private readonly NameValueCollection _queryString = new NameValueCollection();
147154
NameValueCollection IConnectionConfigurationValues.QueryStringParameters => _queryString;
148155

@@ -388,6 +395,35 @@ public T EnableDebugMode(Action<IApiCallDetails> onRequestCompleted = null)
388395
return (T)this;
389396
}
390397

398+
/// <summary>
399+
/// Register a ServerCertificateValidationCallback, this is called per endpoint until it returns true.
400+
/// After this callback returns true that endpoint is validated for the lifetime of the ServiceEndpoint
401+
/// for that host.
402+
/// </summary>
403+
public T ServerCertificateValidationCallback(Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> callback) =>
404+
Assign(a => a._serverCertificateValidationCallback = callback);
405+
406+
/// <summary>
407+
/// Use the following certificates to authenticate all HTTP requests. You can also set them on individual
408+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
409+
/// </summary>
410+
public T ClientCertificates(X509CertificateCollection certificates) =>
411+
Assign(a => a._clientCertificates = certificates);
412+
413+
/// <summary>
414+
/// Use the following certificate to authenticate all HTTP requests. You can also set them on individual
415+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
416+
/// </summary>
417+
public T ClientCertificate(X509Certificate certificate) =>
418+
Assign(a => a._clientCertificates = new X509Certificate2Collection { certificate });
419+
420+
/// <summary>
421+
/// Use the following certificate to authenticate all HTTP requests. You can also set them on individual
422+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
423+
/// </summary>
424+
public T ClientCertificate(string certificatePath) =>
425+
Assign(a => a._clientCertificates = new X509Certificate2Collection { new X509Certificate(certificatePath) });
426+
391427
void IDisposable.Dispose() => this.DisposeManagedResources();
392428

393429
protected virtual void DisposeManagedResources()

src/Elasticsearch.Net/Configuration/IConnectionConfigurationValues.cs

Lines changed: 14 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
@@ -171,5 +173,17 @@ public interface IConnectionConfigurationValues : IDisposable
171173
/// received.
172174
/// </summary>
173175
TimeSpan? KeepAliveInterval { get; }
176+
177+
/// <summary>
178+
/// Register a ServerCertificateValidationCallback per request
179+
/// </summary>
180+
Func<object, X509Certificate,X509Chain,SslPolicyErrors, bool> ServerCertificateValidationCallback { get; }
181+
182+
183+
/// <summary>
184+
/// Use the following certificates to authenticate all HTTP requests. You can also set them on individual
185+
/// request using <see cref="RequestConfiguration.ClientCertificates"/>
186+
/// </summary>
187+
X509CertificateCollection ClientCertificates { get; }
174188
}
175189
}

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
@@ -95,6 +102,8 @@ public class RequestConfiguration : IRequestConfiguration
95102
/// https://www.elastic.co/guide/en/shield/current/submitting-requests-for-other-users.html
96103
/// </summary>
97104
public string RunAs { get; set; }
105+
106+
public X509CertificateCollection ClientCertificates { get; set; }
98107
}
99108

100109
public class RequestConfigurationDescriptor : IRequestConfiguration
@@ -114,6 +123,7 @@ public class RequestConfigurationDescriptor : IRequestConfiguration
114123
bool IRequestConfiguration.EnableHttpPipelining { get; set; } = true;
115124
CancellationToken IRequestConfiguration.CancellationToken { get; set; }
116125
string IRequestConfiguration.RunAs { get; set; }
126+
X509CertificateCollection IRequestConfiguration.ClientCertificates { get; set; }
117127

118128
public RequestConfigurationDescriptor(IRequestConfiguration config)
119129
{
@@ -130,6 +140,7 @@ public RequestConfigurationDescriptor(IRequestConfiguration config)
130140
Self.EnableHttpPipelining = config?.EnableHttpPipelining ?? true;
131141
Self.CancellationToken = config?.CancellationToken ?? default(CancellationToken);
132142
Self.RunAs = config?.RunAs;
143+
Self.ClientCertificates = config?.ClientCertificates;
133144
}
134145

135146
/// <summary>
@@ -195,6 +206,7 @@ public RequestConfigurationDescriptor ForceNode(Uri uri)
195206
Self.ForceNode = uri;
196207
return this;
197208
}
209+
198210
public RequestConfigurationDescriptor MaxRetries(int retry)
199211
{
200212
Self.MaxRetries = retry;
@@ -220,5 +232,20 @@ public RequestConfigurationDescriptor EnableHttpPipelining(bool enable = true)
220232
Self.EnableHttpPipelining = enable;
221233
return this;
222234
}
235+
236+
/// <summary> Use the following client certificates to authenticate this request to Elasticsearch </summary>
237+
public RequestConfigurationDescriptor ClientCertificates(X509CertificateCollection certificates)
238+
{
239+
Self.ClientCertificates = certificates;
240+
return this;
241+
}
242+
243+
/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
244+
public RequestConfigurationDescriptor ClientCertificate(X509Certificate certificate) =>
245+
this.ClientCertificates(new X509Certificate2Collection { certificate });
246+
247+
/// <summary> Use the following client certificate to authenticate this request to Elasticsearch </summary>
248+
public RequestConfigurationDescriptor ClientCertificate(string certificatePath) =>
249+
this.ClientCertificates(new X509Certificate2Collection {new X509Certificate(certificatePath)});
223250
}
224251
}
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)