Skip to content

Commit 35275c2

Browse files
committed
feat(): add mtls configurations for certs
1 parent 3044fc3 commit 35275c2

File tree

5 files changed

+305
-8
lines changed

5 files changed

+305
-8
lines changed

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Security.AccessControl;
88
using System.Security.Cryptography.X509Certificates;
99
using System.Security.Principal;
10+
using Microsoft.Extensions.Configuration;
1011

1112
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
1213

@@ -116,23 +117,44 @@ public static X509Certificate2 LoadClientCertificate(
116117
}
117118

118119
/// <summary>
119-
/// Validates a certificate chain and checks for any errors.
120+
/// Validates the certificate chain for a given certificate.
120121
/// </summary>
121122
/// <param name="certificate">The certificate to validate.</param>
122123
/// <param name="certificateType">Type description for logging (e.g., "Client certificate").</param>
123124
/// <returns>True if the certificate chain is valid; otherwise, false.</returns>
124125
public static bool ValidateCertificateChain(
125126
X509Certificate2 certificate,
126127
string certificateType)
128+
{
129+
return ValidateCertificateChain(certificate, certificateType, null);
130+
}
131+
132+
/// <summary>
133+
/// Validates the certificate chain for a given certificate with optional configuration.
134+
/// </summary>
135+
/// <param name="certificate">The certificate to validate.</param>
136+
/// <param name="certificateType">Type description for logging (e.g., "Client certificate").</param>
137+
/// <param name="configuration">Optional configuration to read environment variables from.</param>
138+
/// <returns>True if the certificate chain is valid; otherwise, false.</returns>
139+
public static bool ValidateCertificateChain(
140+
X509Certificate2 certificate,
141+
string certificateType,
142+
IConfiguration? configuration)
127143
{
128144
try
129145
{
130146
using var chain = new X509Chain();
131147

132148
// Configure chain policy
133149
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
134-
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
135-
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
150+
151+
// Configure RevocationMode from environment variable or use default
152+
var revocationMode = GetRevocationModeFromConfiguration(configuration);
153+
chain.ChainPolicy.RevocationMode = revocationMode;
154+
155+
// Configure RevocationFlag from environment variable or use default
156+
var revocationFlag = GetRevocationFlagFromConfiguration(configuration);
157+
chain.ChainPolicy.RevocationFlag = revocationFlag;
136158

137159
bool isValid = chain.Build(certificate);
138160

@@ -380,6 +402,60 @@ private static void ValidateUnixFilePermissions(string filePath, string fileType
380402
filePath,
381403
"Consider verifying that file permissions are set to 400 (read-only for owner) for enhanced security.");
382404
}
405+
406+
/// <summary>
407+
/// Gets the X509RevocationMode from configuration or returns the default value.
408+
/// </summary>
409+
/// <param name="configuration">Configuration to read from.</param>
410+
/// <returns>The configured revocation mode or default (Online).</returns>
411+
private static X509RevocationMode GetRevocationModeFromConfiguration(IConfiguration? configuration)
412+
{
413+
if (configuration == null)
414+
{
415+
return X509RevocationMode.Online;
416+
}
417+
418+
if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, out var modeString))
419+
{
420+
if (Enum.TryParse<X509RevocationMode>(modeString, true, out var mode))
421+
{
422+
return mode;
423+
}
424+
425+
((IConfigurationExtensionsLogger)OpenTelemetryProtocolExporterEventSource.Log).LogInvalidConfigurationValue(
426+
OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName,
427+
modeString);
428+
}
429+
430+
return X509RevocationMode.Online;
431+
}
432+
433+
/// <summary>
434+
/// Gets the X509RevocationFlag from configuration or returns the default value.
435+
/// </summary>
436+
/// <param name="configuration">Configuration to read from.</param>
437+
/// <returns>The configured revocation flag or default (ExcludeRoot).</returns>
438+
private static X509RevocationFlag GetRevocationFlagFromConfiguration(IConfiguration? configuration)
439+
{
440+
if (configuration == null)
441+
{
442+
return X509RevocationFlag.ExcludeRoot;
443+
}
444+
445+
if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, out var flagString))
446+
{
447+
if (Enum.TryParse<X509RevocationFlag>(flagString, true, out var flag))
448+
{
449+
return flag;
450+
}
451+
452+
((IConfigurationExtensionsLogger)OpenTelemetryProtocolExporterEventSource.Log).LogInvalidConfigurationValue(
453+
OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName,
454+
flagString);
455+
}
456+
457+
return X509RevocationFlag.ExcludeRoot;
458+
}
383459
}
384460

385461
#endif

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ internal static class OtlpSpecConfigDefinitions
3636
public const string CertificateEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE";
3737
public const string ClientKeyEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_KEY";
3838
public const string ClientCertificateEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE";
39+
40+
// Certificate validation environment variables
41+
public const string CertificateRevocationModeEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE";
42+
public const string CertificateRevocationFlagEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG";
3943
}

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,13 @@ or reader
453453
The following environment variables can be used to configure mTLS
454454
(mutual TLS) authentication (.NET 8.0+ only):
455455

456-
| Environment variable | `OtlpMtlsOptions` property | Description |
457-
| ---------------------------------------| ------------------------------|---------------------------------------|
458-
| `OTEL_EXPORTER_OTLP_CERTIFICATE` | `CaCertificatePath` | Path to CA certificate file (PEM) |
459-
| `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | `ClientCertificatePath` | Path to client certificate file (PEM)|
460-
| `OTEL_EXPORTER_OTLP_CLIENT_KEY` | `ClientKeyPath` | Path to client private key file (PEM)|
456+
| Environment variable | `OtlpMtlsOptions` property | Description |
457+
| -----------------------------------------------| ------------------------------|---------------------------------------|
458+
| `OTEL_EXPORTER_OTLP_CERTIFICATE` | `CaCertificatePath` | Path to CA certificate file (PEM) |
459+
| `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | `ClientCertificatePath` | Path to client certificate file (PEM)|
460+
| `OTEL_EXPORTER_OTLP_CLIENT_KEY` | `ClientKeyPath` | Path to client private key file (PEM)|
461+
| `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` |
462+
| `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` |
461463

462464
* Logs:
463465

test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#if NET8_0_OR_GREATER
55

6+
using Microsoft.Extensions.Configuration;
7+
68
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
79

810
public class OtlpMtlsCertificateManagerTests
@@ -157,6 +159,165 @@ public void ValidateCertificateChain_ReturnsResult_WithValidCertificate()
157159
Xunit.Assert.True(result || !result);
158160
}
159161

162+
[Xunit.Fact]
163+
public void ValidateCertificateChain_UsesDefaultConfiguration_WhenConfigurationIsNull()
164+
{
165+
using var cert = CreateSelfSignedCertificate();
166+
167+
// Both overloads should work
168+
var result1 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate");
169+
var result2 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", null);
170+
171+
// Results should be the same since both use defaults
172+
Xunit.Assert.Equal(result1, result2);
173+
}
174+
175+
[Xunit.Fact]
176+
public void ValidateCertificateChain_UsesRevocationModeFromConfiguration()
177+
{
178+
using var cert = CreateSelfSignedCertificate();
179+
180+
var configuration = new ConfigurationBuilder()
181+
.AddInMemoryCollection(new[]
182+
{
183+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "NoCheck"),
184+
})
185+
.Build();
186+
187+
// Should not throw when using NoCheck mode
188+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
189+
190+
// The method should execute without throwing
191+
Xunit.Assert.True(result || !result);
192+
}
193+
194+
[Xunit.Fact]
195+
public void ValidateCertificateChain_UsesRevocationFlagFromConfiguration()
196+
{
197+
using var cert = CreateSelfSignedCertificate();
198+
199+
var configuration = new ConfigurationBuilder()
200+
.AddInMemoryCollection(new[]
201+
{
202+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "EntireChain"),
203+
})
204+
.Build();
205+
206+
// Should not throw when using EntireChain flag
207+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
208+
209+
// The method should execute without throwing
210+
Xunit.Assert.True(result || !result);
211+
}
212+
213+
[Xunit.Fact]
214+
public void ValidateCertificateChain_UsesBothRevocationConfigurationValues()
215+
{
216+
using var cert = CreateSelfSignedCertificate();
217+
218+
var configuration = new ConfigurationBuilder()
219+
.AddInMemoryCollection(new[]
220+
{
221+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "Offline"),
222+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "EndCertificateOnly"),
223+
})
224+
.Build();
225+
226+
// Should not throw when using both configuration values
227+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
228+
229+
// The method should execute without throwing
230+
Xunit.Assert.True(result || !result);
231+
}
232+
233+
[Xunit.Fact]
234+
public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationMode()
235+
{
236+
using var cert = CreateSelfSignedCertificate();
237+
238+
var configuration = new ConfigurationBuilder()
239+
.AddInMemoryCollection(new[]
240+
{
241+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "InvalidMode"),
242+
})
243+
.Build();
244+
245+
// Should not throw even with invalid configuration value (should use default)
246+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
247+
248+
// The method should execute without throwing and use default Online mode
249+
Xunit.Assert.True(result || !result);
250+
}
251+
252+
[Xunit.Fact]
253+
public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationFlag()
254+
{
255+
using var cert = CreateSelfSignedCertificate();
256+
257+
var configuration = new ConfigurationBuilder()
258+
.AddInMemoryCollection(new[]
259+
{
260+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "InvalidFlag"),
261+
})
262+
.Build();
263+
264+
// Should not throw even with invalid configuration value (should use default)
265+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
266+
267+
// The method should execute without throwing and use default ExcludeRoot flag
268+
Xunit.Assert.True(result || !result);
269+
}
270+
271+
[Xunit.Theory]
272+
[Xunit.InlineData("Online")]
273+
[Xunit.InlineData("Offline")]
274+
[Xunit.InlineData("NoCheck")]
275+
[Xunit.InlineData("online")]
276+
[Xunit.InlineData("OFFLINE")]
277+
[Xunit.InlineData("nocheck")]
278+
public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationMode(string revocationMode)
279+
{
280+
using var cert = CreateSelfSignedCertificate();
281+
282+
var configuration = new ConfigurationBuilder()
283+
.AddInMemoryCollection(new[]
284+
{
285+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, revocationMode),
286+
})
287+
.Build();
288+
289+
// Should handle case-insensitive enum parsing
290+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
291+
292+
// The method should execute without throwing
293+
Xunit.Assert.True(result || !result);
294+
}
295+
296+
[Xunit.Theory]
297+
[Xunit.InlineData("ExcludeRoot")]
298+
[Xunit.InlineData("EntireChain")]
299+
[Xunit.InlineData("EndCertificateOnly")]
300+
[Xunit.InlineData("excluderoot")]
301+
[Xunit.InlineData("ENTIRECHAIN")]
302+
[Xunit.InlineData("endcertificateonly")]
303+
public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationFlag(string revocationFlag)
304+
{
305+
using var cert = CreateSelfSignedCertificate();
306+
307+
var configuration = new ConfigurationBuilder()
308+
.AddInMemoryCollection(new[]
309+
{
310+
new KeyValuePair<string, string?>(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, revocationFlag),
311+
})
312+
.Build();
313+
314+
// Should handle case-insensitive enum parsing
315+
var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration);
316+
317+
// The method should execute without throwing
318+
Xunit.Assert.True(result || !result);
319+
}
320+
160321
private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSignedCertificate()
161322
{
162323
using var rsa = System.Security.Cryptography.RSA.Create(2048);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NET8_0_OR_GREATER
5+
6+
using Xunit;
7+
8+
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
9+
10+
public class OtlpSpecConfigDefinitionsTests
11+
{
12+
[Fact]
13+
public void CertificateRevocationModeEnvVarName_HasCorrectValue()
14+
{
15+
Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE", OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName);
16+
}
17+
18+
[Fact]
19+
public void CertificateRevocationFlagEnvVarName_HasCorrectValue()
20+
{
21+
Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName);
22+
}
23+
24+
[Fact]
25+
public void AllEnvironmentVariableNames_AreUnique()
26+
{
27+
var envVars = new[]
28+
{
29+
OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName,
30+
OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName,
31+
OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName,
32+
OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName,
33+
OtlpSpecConfigDefinitions.CertificateEnvVarName,
34+
OtlpSpecConfigDefinitions.ClientKeyEnvVarName,
35+
OtlpSpecConfigDefinitions.ClientCertificateEnvVarName,
36+
OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName,
37+
OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName,
38+
};
39+
40+
var uniqueVars = envVars.Distinct().ToArray();
41+
42+
Assert.Equal(envVars.Length, uniqueVars.Length);
43+
}
44+
45+
[Fact]
46+
public void CertificateRevocationEnvironmentVariables_FollowNamingConvention()
47+
{
48+
// All certificate-related environment variables should follow the OTEL_EXPORTER_OTLP_CERTIFICATE prefix
49+
Assert.StartsWith("OTEL_EXPORTER_OTLP_CERTIFICATE", OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, StringComparison.Ordinal);
50+
Assert.StartsWith("OTEL_EXPORTER_OTLP_CERTIFICATE", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, StringComparison.Ordinal);
51+
}
52+
}
53+
54+
#endif

0 commit comments

Comments
 (0)