Skip to content

Commit 23037c1

Browse files
feat: add custom SSL root certificate support (#153)
* feat: add custom SSL root certificate support * fix: lint errors * fix: tests * test: add temporary debug info * feat: add DisableCertificateRevocationListCheck * feat: add DisableCertificateRevocationListCheck * fix: disable revocation errors if disabled * test: more debug info * test: more debug info * test: remove debug info * test: add more tests * test: add more tests * doc: update CHANGELOG.md * fix: warnings * docs: format doc-comments to render options as list in ClientConfig.cs --------- Co-authored-by: karel rehor <[email protected]>
1 parent 82f46ef commit 23037c1

23 files changed

+1031
-16
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
## 1.1.0 [unreleased]
22

3+
### Features
4+
5+
1. [#153](https://github.com/InfluxCommunity/influxdb3-csharp/pull/153): Add custom SSL root certificate support.
6+
- New configuration items:
7+
- `SslRootsFilePath`
8+
- `DisableCertificateRevocationListCheck`
9+
- **Disclaimer:** Using custom SSL root certificate configurations is recommended for development and testing
10+
purposes
11+
only. For production deployments, ensure custom certificates are added to the operating system's trusted
12+
certificate store.
13+
314
## 1.0.0 [2025-01-22]
415

516
### Features

Client.Test/Client.Test.csproj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,25 @@
2828
<ProjectReference Include="..\Client\Client.csproj" />
2929
</ItemGroup>
3030

31+
<ItemGroup>
32+
<None Update="TestData\ServerCert\server.p12">
33+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
34+
</None>
35+
<None Update="TestData\ServerCert\server.pem">
36+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
37+
</None>
38+
<None Update="TestData\ServerCert\rootCA.pem">
39+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
40+
</None>
41+
<None Update="TestData\OtherCerts\otherCA.pem">
42+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
43+
</None>
44+
<None Update="TestData\OtherCerts\empty.pem">
45+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
46+
</None>
47+
<None Update="TestData\OtherCerts\invalid.pem">
48+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
49+
</None>
50+
</ItemGroup>
51+
3152
</Project>
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
using System;
2+
using System.Linq;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Grpc.Core;
6+
using InfluxDB3.Client.Config;
7+
using WireMock.RequestBuilders;
8+
using WireMock.ResponseBuilders;
9+
10+
namespace InfluxDB3.Client.Test;
11+
12+
public class InfluxDBClientHttpsTest : MockHttpsServerTest
13+
{
14+
private InfluxDBClient _client;
15+
16+
[TearDown]
17+
public new void TearDown()
18+
{
19+
_client?.Dispose();
20+
}
21+
22+
[Test]
23+
public Task NonExistingCertificateFile()
24+
{
25+
var ae = Assert.ThrowsAsync<ArgumentException>(() =>
26+
{
27+
_client = new InfluxDBClient(new ClientConfig
28+
{
29+
Host = MockHttpsServerUrl,
30+
Token = "my-token",
31+
Organization = "my-org",
32+
Database = "my-database",
33+
DisableServerCertificateValidation = false,
34+
DisableCertificateRevocationListCheck = true,
35+
SslRootsFilePath = "./not-existing.pem"
36+
});
37+
return null;
38+
});
39+
40+
Assert.That(ae, Is.Not.Null);
41+
Assert.That(ae.Message, Does.Match("Certificate file '.*' not found"));
42+
return Task.CompletedTask;
43+
}
44+
45+
[Test]
46+
public Task EmptyCertificateFile()
47+
{
48+
var ae = Assert.ThrowsAsync<ArgumentException>(() =>
49+
{
50+
_client = new InfluxDBClient(new ClientConfig
51+
{
52+
Host = MockHttpsServerUrl,
53+
Token = "my-token",
54+
Organization = "my-org",
55+
Database = "my-database",
56+
DisableServerCertificateValidation = false,
57+
DisableCertificateRevocationListCheck = true,
58+
SslRootsFilePath = "./TestData/OtherCerts/empty.pem"
59+
});
60+
return null;
61+
});
62+
63+
Assert.That(ae, Is.Not.Null);
64+
Assert.That(ae.Message, Does.Match("Certificate file '.*' is empty"));
65+
return Task.CompletedTask;
66+
}
67+
68+
[Test]
69+
public Task InvalidCertificateFile()
70+
{
71+
try
72+
{
73+
_client = new InfluxDBClient(new ClientConfig
74+
{
75+
Host = MockHttpsServerUrl,
76+
Token = "my-token",
77+
Organization = "my-org",
78+
Database = "my-database",
79+
DisableServerCertificateValidation = false,
80+
DisableCertificateRevocationListCheck = true,
81+
SslRootsFilePath = "./TestData/OtherCerts/invalid.pem"
82+
});
83+
}
84+
catch (Exception e)
85+
{
86+
Assert.That(e, Is.Not.Null);
87+
Assert.That(e.Message, Does.Match("Failed to import custom certificates"));
88+
}
89+
90+
return Task.CompletedTask;
91+
}
92+
93+
[Test]
94+
public async Task WriteWithValidSslRootCertificate()
95+
{
96+
_client = new InfluxDBClient(new ClientConfig
97+
{
98+
Host = MockHttpsServerUrl,
99+
Token = "my-token",
100+
Organization = "my-org",
101+
Database = "my-database",
102+
DisableServerCertificateValidation = false,
103+
DisableCertificateRevocationListCheck = true,
104+
SslRootsFilePath = "./TestData/ServerCert/rootCA.pem"
105+
});
106+
107+
await WriteData();
108+
109+
var requests = MockHttpsServer.LogEntries.ToList();
110+
Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Is.EqualTo("mem,tag=a field=1"));
111+
}
112+
113+
[Test]
114+
public async Task WriteWithDisabledCertificates()
115+
{
116+
_client = new InfluxDBClient(new ClientConfig
117+
{
118+
Host = MockHttpsServerUrl,
119+
Token = "my-token",
120+
Organization = "my-org",
121+
Database = "my-database",
122+
DisableServerCertificateValidation = true,
123+
});
124+
125+
await WriteData();
126+
127+
var requests = MockHttpsServer.LogEntries.ToList();
128+
Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Is.EqualTo("mem,tag=a field=1"));
129+
}
130+
131+
[Test]
132+
public Task WriteWithOtherSslRootCertificate()
133+
{
134+
_client = new InfluxDBClient(new ClientConfig
135+
{
136+
Host = MockHttpsServerUrl,
137+
Token = "my-token",
138+
Organization = "my-org",
139+
Database = "my-database",
140+
DisableServerCertificateValidation = false,
141+
DisableCertificateRevocationListCheck = true,
142+
SslRootsFilePath = "./TestData/OtherCerts/otherCA.pem"
143+
});
144+
145+
var ae = Assert.ThrowsAsync<HttpRequestException>(async () =>
146+
{
147+
await _client.WriteRecordAsync("mem,tag=a field=1");
148+
});
149+
150+
Assert.That(ae, Is.Not.Null);
151+
Assert.That(ae.Message, Does.Contain("The SSL connection could not be established"));
152+
Assert.That(ae.InnerException?.Message,
153+
Does.Contain("The remote certificate was rejected by the provided RemoteCertificateValidationCallback"));
154+
return Task.CompletedTask;
155+
}
156+
157+
[Test]
158+
public Task QueryWithValidSslRootCertificate()
159+
{
160+
_client = new InfluxDBClient(new ClientConfig
161+
{
162+
Host = MockHttpsServerUrl,
163+
Token = "my-token",
164+
Organization = "my-org",
165+
Database = "my-database",
166+
DisableServerCertificateValidation = false,
167+
DisableCertificateRevocationListCheck = true,
168+
SslRootsFilePath = "./TestData/ServerCert/rootCA.pem"
169+
});
170+
171+
var ae = Assert.ThrowsAsync<RpcException>(async () => { await QueryData(); });
172+
173+
// Verify: server successfully sent back the configured 404 status
174+
Assert.That(ae, Is.Not.Null);
175+
Assert.That(ae.Message, Does.Contain("Bad gRPC response. HTTP status code: 404"));
176+
177+
// Verify: the request reached the server
178+
var requests = MockHttpsServer.LogEntries.ToList();
179+
Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Does.Contain("SELECT 1"));
180+
return Task.CompletedTask;
181+
}
182+
183+
[Test]
184+
public Task QueryWithDisabledCertificates()
185+
{
186+
_client = new InfluxDBClient(new ClientConfig
187+
{
188+
Host = MockHttpsServerUrl,
189+
Token = "my-token",
190+
Organization = "my-org",
191+
Database = "my-database",
192+
DisableServerCertificateValidation = true,
193+
});
194+
195+
var ae = Assert.ThrowsAsync<RpcException>(async () => { await QueryData(); });
196+
197+
// Verify: server successfully sent back the configured 404 status
198+
Assert.That(ae, Is.Not.Null);
199+
Assert.That(ae.Message, Does.Contain("Bad gRPC response. HTTP status code: 404"));
200+
201+
// Verify: the request reached the server
202+
var requests = MockHttpsServer.LogEntries.ToList();
203+
Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Does.Contain("SELECT 1"));
204+
return Task.CompletedTask;
205+
}
206+
207+
[Test]
208+
public Task QueryWithOtherSslRootCertificate()
209+
{
210+
_client = new InfluxDBClient(new ClientConfig
211+
{
212+
Host = MockHttpsServerUrl,
213+
Token = "my-token",
214+
Organization = "my-org",
215+
Database = "my-database",
216+
DisableServerCertificateValidation = false,
217+
DisableCertificateRevocationListCheck = true,
218+
SslRootsFilePath = "./TestData/OtherCerts/otherCA.pem"
219+
});
220+
221+
var ae = Assert.ThrowsAsync<RpcException>(async () => { await QueryData(); });
222+
223+
// Verify: the SSL connection was not established
224+
Assert.That(ae, Is.Not.Null);
225+
Assert.That(ae.Message, Does.Contain("The SSL connection could not be established"));
226+
227+
// Verify: the request did not reach the server
228+
var requests = MockHttpsServer.LogEntries.ToList();
229+
Assert.That(requests, Is.Empty);
230+
return Task.CompletedTask;
231+
}
232+
233+
private async Task WriteData()
234+
{
235+
MockHttpsServer
236+
.Given(Request.Create().WithPath("/api/v2/write").UsingPost())
237+
.RespondWith(Response.Create().WithStatusCode(204));
238+
239+
await _client.WriteRecordAsync("mem,tag=a field=1");
240+
}
241+
242+
243+
private async Task QueryData()
244+
{
245+
// Setup mock server: return 404 for simplicity, so we don't have to implement a valid response.
246+
MockHttpsServer
247+
.Given(Request.Create().WithPath("/arrow.flight.protocol.FlightService/DoGet").UsingPost())
248+
.RespondWith(Response.Create()
249+
.WithStatusCode(404)
250+
);
251+
252+
// Query data.
253+
var query = "SELECT 1";
254+
await _client.Query(query).ToListAsync();
255+
}
256+
}

0 commit comments

Comments
 (0)