Skip to content

Commit 96858ed

Browse files
zivillianniemyjski
authored andcommitted
Server certificate validation (#194)
* add certificate validation callback * add unit test for cert validation * add config extension for certificate validation configuration * cleanup * fix race condition * remove local functions
1 parent 45e46d8 commit 96858ed

File tree

6 files changed

+200
-2
lines changed

6 files changed

+200
-2
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#if !PORTABLE && !NETSTANDARD1_2
2+
using System.Net.Http;
3+
using System.Net.Security;
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace Exceptionless {
7+
public class CertificateData {
8+
#if NET45
9+
public CertificateData(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
10+
:this(chain, sslPolicyErrors) {
11+
Sender = sender;
12+
Certificate = new X509Certificate2(certificate.Handle);
13+
}
14+
#else
15+
public CertificateData(HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
16+
:this(chain, sslPolicyErrors) {
17+
Request = request;
18+
Certificate = certificate;
19+
}
20+
#endif
21+
private CertificateData(X509Chain chain, SslPolicyErrors sslPolicyErrors) {
22+
Chain = chain;
23+
SslPolicyErrors = sslPolicyErrors;
24+
}
25+
26+
/// <summary>
27+
/// The certificate used to authenticate the remote party.
28+
/// </summary>
29+
public X509Certificate2 Certificate { get; }
30+
31+
/// <summary>
32+
/// The chain of certificate authorities associated with the remote certificate.
33+
/// </summary>
34+
public X509Chain Chain { get; }
35+
36+
/// <summary>
37+
/// One or more errors associated with the remote certificate.
38+
/// </summary>
39+
public SslPolicyErrors SslPolicyErrors { get; }
40+
41+
#if NET45
42+
/// <summary>
43+
/// An object that contains state information for this validation.
44+
/// </summary>
45+
public object Sender { get; }
46+
#endif
47+
48+
#if !NET45 && !PORTABLE && !NETSTANDARD1_2
49+
/// <summary>
50+
/// The request which was sent to the remore party
51+
/// </summary>
52+
public HttpRequestMessage Request { get; }
53+
#endif
54+
}
55+
}
56+
#endif

src/Exceptionless/Configuration/ExceptionlessConfiguration.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
#if !PORTABLE
23
using System.Collections.Concurrent;
4+
#endif
35
using System.Collections.Generic;
46
using System.Diagnostics;
57
using System.Linq;
@@ -275,6 +277,13 @@ public int SubmissionBatchSize {
275277
}
276278
}
277279

280+
#if !PORTABLE && !NETSTANDARD1_2
281+
/// <summary>
282+
/// Callback which is invoked to validate the exceptionless server certificate.
283+
/// </summary>
284+
public Func<CertificateData, bool> ServerCertificateValidationCallback { get; set; }
285+
#endif
286+
278287
/// <summary>
279288
/// A list of exclusion patterns that will automatically remove any data that matches them from any data submitted to the server.
280289
/// For example, entering CreditCard will remove any extended data properties, form fields, cookies and query

src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#if !PORTABLE && !NETSTANDARD1_2
1717
using Exceptionless.Diagnostics;
18+
using System.Net.Security;
1819
#endif
1920

2021
#if NET45
@@ -435,6 +436,46 @@ private static string GetEnvironmentalVariable(string name) {
435436
}
436437
#endif
437438

439+
#if !PORTABLE && !NETSTANDARD1_2
440+
/// <summary>
441+
/// Add a custom server certificate validation against the thumbprint of the server certificate.
442+
/// </summary>
443+
/// <param name="config">The configuration object you want to apply the attribute settings to.</param>
444+
/// <param name="thumbprint">Thumbprint of the server certificate. <example>e.g. "86481791CDAF6D7A02BEE9A649EA9F84DE84D22C"</example></param>
445+
public static void TrustCertificateThumbprint(this ExceptionlessConfiguration config, string thumbprint) {
446+
config.ServerCertificateValidationCallback = x => {
447+
if (x.SslPolicyErrors == SslPolicyErrors.None) return true;
448+
return x.Certificate != null && thumbprint != null && thumbprint.Equals(x.Certificate.Thumbprint, StringComparison.OrdinalIgnoreCase);
449+
};
450+
}
451+
452+
/// <summary>
453+
/// Add a custom server certificate validation against the thumbprint of any of the ca certificates.
454+
/// </summary>
455+
/// <param name="config">The configuration object you want to apply the attribute settings to.</param>
456+
/// <param name="thumbprint">Thumbprint of the ca certificate. <example>e.g. "afe5d244a8d1194230ff479fe2f897bbcd7a8cb4"</example></param>
457+
public static void TrustCAThumbprint(this ExceptionlessConfiguration config, string thumbprint) {
458+
config.ServerCertificateValidationCallback = x => {
459+
if (x.SslPolicyErrors == SslPolicyErrors.None) return true;
460+
if (x.Chain == null || thumbprint == null) return false;
461+
foreach (var ca in x.Chain.ChainElements) {
462+
if (thumbprint.Equals(ca.Certificate.Thumbprint, StringComparison.OrdinalIgnoreCase))
463+
return true;
464+
}
465+
return false;
466+
};
467+
}
468+
469+
/// <summary>
470+
/// Disable any certificate validation. Do not use this in production.
471+
/// </summary>
472+
/// <param name="config"></param>
473+
[Obsolete("This will open the client to Man-in-Middle attacks. It should never be used in production.")]
474+
public static void SkipCertificateValidation(this ExceptionlessConfiguration config) {
475+
config.ServerCertificateValidationCallback = x => true;
476+
}
477+
#endif
478+
438479
private static bool IsValidApiKey(string apiKey) {
439480
return !String.IsNullOrEmpty(apiKey) && apiKey != "API_KEY_HERE";
440481
}

src/Exceptionless/Newtonsoft.Json/Serialization/DefaultContractResolver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
#if !(DOTNET || PORTABLE || PORTABLE40 || NETSTANDARD1_0 || NETSTANDARD1_1 || NETSTANDARD1_2 || NETSTANDARD1_3 || NETSTANDARD1_4 || NETSTANDARD1_5)
4141
using System.Security.Permissions;
4242
#endif
43+
#if !PORTABLE
4344
using System.Xml.Serialization;
45+
#endif
4446
using Exceptionless.Json.Converters;
4547
using Exceptionless.Json.Utilities;
4648
using Exceptionless.Json.Linq;

src/Exceptionless/Submission/DefaultSubmissionClient.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
using System.Net;
55
using System.Net.Http;
66
using System.Net.Http.Headers;
7+
#if NET45 || (!PORTABLE && !NETSTANDARD1_2)
8+
using System.Net.Security;
9+
using System.Security.Cryptography.X509Certificates;
10+
#endif
711
using System.Text;
812
using Exceptionless.Configuration;
913
using Exceptionless.Dependency;
@@ -123,12 +127,18 @@ public void SendHeartbeat(string sessionIdOrUserId, bool closeSession, Exception
123127
protected virtual HttpClient CreateHttpClient(ExceptionlessConfiguration config) {
124128
#if NET45
125129
var handler = new WebRequestHandler { UseDefaultCredentials = true };
126-
handler.ServerCertificateValidationCallback = delegate { return true; };
127130
#else
128131
var handler = new HttpClientHandler { UseDefaultCredentials = true };
132+
#endif
129133
#if !PORTABLE && !NETSTANDARD1_2
130-
//handler.ServerCertificateCustomValidationCallback = delegate { return true; };
134+
var callback = config.ServerCertificateValidationCallback;
135+
if (callback != null) {
136+
#if NET45
137+
handler.ServerCertificateValidationCallback = (s,c,ch,p)=>Validate(s,c,ch,p,callback);
138+
#else
139+
handler.ServerCertificateCustomValidationCallback = (m,c,ch,p)=>Validate(m,c,ch,p,callback);
131140
#endif
141+
}
132142
#endif
133143
if (handler.SupportsAutomaticDecompression)
134144
handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.None;
@@ -147,6 +157,20 @@ protected virtual HttpClient CreateHttpClient(ExceptionlessConfiguration config)
147157
return client;
148158
}
149159

160+
#if !PORTABLE && !NETSTANDARD1_2
161+
#if NET45
162+
private bool Validate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors, Func<CertificateData, bool> callback) {
163+
var certData = new CertificateData(sender, certificate, chain, sslPolicyErrors);
164+
return callback(certData);
165+
}
166+
#else
167+
private bool Validate(HttpRequestMessage httpRequestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors, Func<CertificateData, bool> callback) {
168+
var certData = new CertificateData(httpRequestMessage, certificate, chain, sslPolicyErrors);
169+
return callback(certData);
170+
}
171+
#endif
172+
#endif
173+
150174
private string GetResponseMessage(HttpResponseMessage response) {
151175
if (response.IsSuccessStatusCode)
152176
return null;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Net.Security;
3+
using Exceptionless.Dependency;
4+
using Exceptionless.Submission;
5+
using Xunit;
6+
7+
namespace Exceptionless.Tests.Configuration {
8+
public class CertificateValidationTest {
9+
[Fact(Skip = "depends on availability of a remote service")]
10+
public void CanOverrideCertValidation() {
11+
Run(GetClient(x => {
12+
Assert.NotNull(x.Certificate);
13+
Assert.NotNull(x.Chain);
14+
Assert.NotNull(x.Sender);
15+
Assert.Equal(SslPolicyErrors.RemoteCertificateChainErrors, x.SslPolicyErrors);
16+
return true;
17+
},"https://expired.badssl.com/"));
18+
}
19+
20+
[Fact(Skip = "depends on availability of a remote service")]
21+
public void CanTrustByThumbprint() {
22+
Run(GetClient(x=>x.Certificate.Thumbprint == "3E8AB453B8CF62F0BD0240739AAB815A170B08F0", "https://revoked.badssl.com/"));
23+
var client = GetClient(null, "https://revoked.badssl.com/");
24+
client.Configuration.TrustCertificateThumbprint("3e8Ab453b8cf62f0bd0240739aab815a170b08f0");
25+
Run(client);
26+
}
27+
28+
[Fact(Skip = "depends on availability of a remote service")]
29+
public void CanTrustByCAThumbprint() {
30+
var client = GetClient(null, "https://revoked.badssl.com/");
31+
client.Configuration.TrustCAThumbprint("a8985d3A65e5e5c4b2d7d66d40c6dd2fb19c5436");
32+
Run(client);
33+
}
34+
35+
[Fact(Skip = "depends on availability of a remote service")]
36+
public void CanTrustAllCertificates() {
37+
var client = GetClient(null, "https://self-signed.badssl.com/");
38+
#pragma warning disable CS0618 // 'member' is obsolete
39+
client.Configuration.SkipCertificateValidation();
40+
#pragma warning restore CS0618 // 'member' is obsolete
41+
Run(client);
42+
}
43+
44+
private void Run(ExceptionlessClient client) {
45+
bool failed = true;
46+
var callback = client.Configuration.ServerCertificateValidationCallback;
47+
client.Configuration.ServerCertificateValidationCallback = x => {
48+
failed = false;
49+
return callback(x);
50+
};
51+
var submissionClient = client.Configuration.Resolver.Resolve<ISubmissionClient>();
52+
var response = submissionClient.GetSettings(client.Configuration, 1, null);
53+
Assert.Contains(" 404 ", response.Message);
54+
Assert.False(failed, "Validation Callback was not invoked");
55+
}
56+
57+
private ExceptionlessClient GetClient(Func<CertificateData, bool> validator, string serverUrl) {
58+
return new ExceptionlessClient("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw") {
59+
Configuration = {
60+
ServerUrl = serverUrl,
61+
ServerCertificateValidationCallback = validator
62+
}
63+
};
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)