diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
index 891b8b7e6f9..c394cdf20bd 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
@@ -325,5 +325,28 @@ internal void MtlsHttpClientCreationFailed(Exception ex)
Level = EventLevel.Error)]
internal void MtlsHttpClientCreationFailed(string exception) =>
this.WriteEvent(34, exception);
+
+ [Event(
+ 35,
+ Message = "CA configured for server validation. Subject: '{0}'.",
+ Level = EventLevel.Informational)]
+ internal void CaCertificateConfigured(string subject) =>
+ this.WriteEvent(35, subject);
+
+ [NonEvent]
+ internal void SecureHttpClientCreationFailed(Exception ex)
+ {
+ if (Log.IsEnabled(EventLevel.Error, EventKeywords.All))
+ {
+ this.SecureHttpClientCreationFailed(ex.ToInvariantString());
+ }
+ }
+
+ [Event(
+ 36,
+ Message = "Failed to create secure HttpClient. Exception: {0}",
+ Level = EventLevel.Error)]
+ internal void SecureHttpClientCreationFailed(string exception) =>
+ this.WriteEvent(36, exception);
#endif
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpCertificateManager.cs
similarity index 95%
rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs
rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpCertificateManager.cs
index b394068e1c4..d445e4d309d 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpCertificateManager.cs
@@ -10,11 +10,15 @@
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
///
-/// Manages certificate loading, validation, and security checks for mTLS connections.
+/// Manages certificate loading, validation, and security checks for TLS connections.
///
-internal static class OtlpMtlsCertificateManager
+///
+/// This class provides functionality for both simple server certificate trust
+/// (for self-signed certificates) and mTLS client authentication scenarios.
+///
+internal static class OtlpCertificateManager
{
- internal const string CaCertificateType = "CA certificate";
+ internal const string CaCertificateType = "CA Certificate";
internal const string ClientCertificateType = "Client certificate";
internal const string ClientPrivateKeyType = "Client private key";
@@ -218,6 +222,10 @@ public static bool ValidateCertificateChain(
/// The SSL policy errors.
/// The CA certificate to validate against.
/// True if the certificate is valid; otherwise, false.
+ ///
+ /// This method is used to validate server certificates against a CA.
+ /// Common use case: connecting to a server with a self-signed certificate.
+ ///
internal static bool ValidateServerCertificate(
X509Certificate2 serverCert,
X509Chain chain,
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs
deleted file mode 100644
index fbf46ede410..00000000000
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright The OpenTelemetry Authors
-// SPDX-License-Identifier: Apache-2.0
-
-#if NET
-
-using System.Security.Cryptography.X509Certificates;
-
-namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
-
-///
-/// Factory for creating HttpClient instances configured with mTLS settings.
-///
-internal static class OtlpMtlsHttpClientFactory
-{
- ///
- /// Creates an HttpClient configured with mTLS settings.
- ///
- /// The mTLS configuration options.
- /// Optional action to configure the client.
- /// An HttpClient configured for mTLS.
- /// Thrown when is null.
- /// Thrown when mTLS is not enabled.
- public static HttpClient CreateMtlsHttpClient(
- OtlpMtlsOptions mtlsOptions,
- Action? configureClient = null)
- {
- ArgumentNullException.ThrowIfNull(mtlsOptions);
-
- if (!mtlsOptions.IsEnabled)
- {
- throw new InvalidOperationException("mTLS options must include a client or CA certificate path.");
- }
-
- HttpClientHandler? handler = null;
- X509Certificate2? caCertificate = null;
- X509Certificate2? clientCertificate = null;
-
- try
- {
- // Load certificates
- if (!string.IsNullOrEmpty(mtlsOptions.CaCertificatePath))
- {
- caCertificate = OtlpMtlsCertificateManager.LoadCaCertificate(
- mtlsOptions.CaCertificatePath);
-
- if (mtlsOptions.EnableCertificateChainValidation)
- {
- OtlpMtlsCertificateManager.ValidateCertificateChain(
- caCertificate,
- OtlpMtlsCertificateManager.CaCertificateType);
- }
- }
-
- if (!string.IsNullOrEmpty(mtlsOptions.ClientCertificatePath))
- {
- if (string.IsNullOrEmpty(mtlsOptions.ClientKeyPath))
- {
- // Load certificate without separate key file (e.g., PKCS#12 format)
- clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate(
- mtlsOptions.ClientCertificatePath,
- null);
- }
- else
- {
- clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate(
- mtlsOptions.ClientCertificatePath,
- mtlsOptions.ClientKeyPath);
- }
-
- if (mtlsOptions.EnableCertificateChainValidation)
- {
- OtlpMtlsCertificateManager.ValidateCertificateChain(
- clientCertificate,
- OtlpMtlsCertificateManager.ClientCertificateType);
- }
-
- OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled(
- clientCertificate.Subject);
- }
-
- // Create HttpClientHandler with mTLS configuration
-#pragma warning disable CA2000 // Dispose objects before losing scope - HttpClientHandler is disposed by HttpClient
- handler = new MtlsHttpClientHandler(clientCertificate, caCertificate);
-#pragma warning restore CA2000
- handler.CheckCertificateRevocationList = true;
-
- // Handler now owns the certificates and will dispose them when disposed.
- caCertificate = null;
- clientCertificate = null;
-
- var client = new HttpClient(handler, disposeHandler: true);
-
- configureClient?.Invoke(client);
-
- return client;
- }
- catch (Exception ex)
- {
- // Dispose handler if something went wrong
- handler?.Dispose();
-
- OpenTelemetryProtocolExporterEventSource.Log.MtlsHttpClientCreationFailed(ex);
- throw;
- }
- finally
- {
- // Dispose certificates as they are no longer needed after being added to the handler
- caCertificate?.Dispose();
- clientCertificate?.Dispose();
- }
- }
-
- private sealed class MtlsHttpClientHandler : HttpClientHandler
- {
- private readonly X509Certificate2? caCertificate;
- private readonly X509Certificate2? clientCertificate;
-
- internal MtlsHttpClientHandler(
- X509Certificate2? clientCertificate,
- X509Certificate2? caCertificate)
- {
- this.clientCertificate = clientCertificate;
- this.caCertificate = caCertificate;
- this.CheckCertificateRevocationList = true;
-
- if (clientCertificate != null)
- {
- this.ClientCertificates.Add(clientCertificate);
- this.ClientCertificateOptions = ClientCertificateOption.Manual;
- }
-
- if (caCertificate != null)
- {
- this.ServerCertificateCustomValidationCallback = (
- httpRequestMessage,
- cert,
- chain,
- sslPolicyErrors) =>
- {
- if (cert == null || chain == null)
- {
- return false;
- }
-
- return OtlpMtlsCertificateManager.ValidateServerCertificate(
- cert,
- chain,
- sslPolicyErrors,
- caCertificate);
- };
- }
- }
-
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- this.caCertificate?.Dispose();
- this.clientCertificate?.Dispose();
- }
-
- base.Dispose(disposing);
- }
- }
-}
-
-#endif
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSecureHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSecureHttpClientFactory.cs
new file mode 100644
index 00000000000..143ef37633f
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSecureHttpClientFactory.cs
@@ -0,0 +1,205 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+using System.Security.Cryptography.X509Certificates;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
+
+///
+/// Factory for creating HttpClient instances configured with TLS settings.
+///
+internal static class OtlpSecureHttpClientFactory
+{
+ ///
+ /// Creates an HttpClient configured with TLS settings based on the provided options.
+ ///
+ /// The TLS configuration options.
+ /// Optional action to configure the client.
+ /// An HttpClient configured for secure communication.
+ /// Thrown when is null.
+ /// Thrown when TLS is not enabled.
+ public static HttpClient CreateSecureHttpClient(
+ OtlpTlsOptions tlsOptions,
+ Action? configureClient = null)
+ {
+ ArgumentNullException.ThrowIfNull(tlsOptions);
+
+ if (!tlsOptions.IsTlsEnabled && !tlsOptions.IsMtlsEnabled)
+ {
+ throw new InvalidOperationException(
+ "TLS options must include at least a CA path or client certificate path.");
+ }
+
+ X509Certificate2? caCertificate = null;
+ X509Certificate2? clientCertificate = null;
+ TlsHttpClientHandler? handler = null;
+
+ try
+ {
+ if (!string.IsNullOrEmpty(tlsOptions.CaCertificatePath))
+ {
+ caCertificate = OtlpCertificateManager.LoadCaCertificate(
+ tlsOptions.CaCertificatePath);
+
+ if (tlsOptions.EnableCertificateChainValidation)
+ {
+ OtlpCertificateManager.ValidateCertificateChain(
+ caCertificate,
+ OtlpCertificateManager.CaCertificateType);
+ }
+ }
+
+ if (tlsOptions is OtlpMtlsOptions mtlsOptions && mtlsOptions.IsMtlsEnabled)
+ {
+ clientCertificate = string.IsNullOrEmpty(mtlsOptions.ClientKeyPath)
+ ? OtlpCertificateManager.LoadClientCertificate(
+ mtlsOptions.ClientCertificatePath!,
+ null)
+ : OtlpCertificateManager.LoadClientCertificate(
+ mtlsOptions.ClientCertificatePath!,
+ mtlsOptions.ClientKeyPath);
+
+ if (tlsOptions.EnableCertificateChainValidation)
+ {
+ OtlpCertificateManager.ValidateCertificateChain(
+ clientCertificate,
+ OtlpCertificateManager.ClientCertificateType);
+ }
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled(
+ clientCertificate.Subject);
+ }
+ else if (caCertificate != null)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.CaCertificateConfigured(
+ caCertificate.Subject);
+ }
+
+ // Create HttpClientHandler and apply TLS configuration
+#pragma warning disable CA2000 // Dispose objects before losing scope - HttpClientHandler is disposed by HttpClient
+ handler = new TlsHttpClientHandler(caCertificate, clientCertificate);
+#pragma warning restore CA2000
+
+ // Handler now owns certificates
+ caCertificate = null;
+ clientCertificate = null;
+
+#pragma warning disable CA5399 // CheckCertificateRevocationList is set in ConfigureTls.
+ var client = new HttpClient(handler, disposeHandler: true);
+#pragma warning restore CA5399
+
+ configureClient?.Invoke(client);
+
+ return client;
+ }
+ catch (Exception ex)
+ {
+ // Clean up on failure
+ handler?.Dispose();
+ caCertificate?.Dispose();
+ clientCertificate?.Dispose();
+
+ OpenTelemetryProtocolExporterEventSource.Log.SecureHttpClientCreationFailed(ex);
+ throw;
+ }
+ }
+
+ ///
+ /// Creates an HttpClient configured with mTLS settings.
+ ///
+ /// The mTLS configuration options.
+ /// Optional action to configure the client.
+ /// An HttpClient configured for mTLS.
+ ///
+ /// This method exists for backward compatibility. New code should use
+ /// .
+ ///
+ public static HttpClient CreateMtlsHttpClient(
+ OtlpMtlsOptions mtlsOptions,
+ Action? configureClient = null)
+ {
+ return CreateSecureHttpClient(mtlsOptions, configureClient);
+ }
+
+ ///
+ /// HttpClientHandler that applies TLS configuration based on loaded certificates.
+ ///
+ private sealed class TlsHttpClientHandler : HttpClientHandler
+ {
+ private readonly X509Certificate2? caCertificate;
+ private readonly X509Certificate2? clientCertificate;
+ private bool disposed;
+
+ internal TlsHttpClientHandler(
+ X509Certificate2? caCertificate,
+ X509Certificate2? clientCertificate)
+ {
+ this.caCertificate = caCertificate;
+ this.clientCertificate = clientCertificate;
+
+ this.ConfigureTls();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && !this.disposed)
+ {
+ this.clientCertificate?.Dispose();
+ this.caCertificate?.Dispose();
+ this.disposed = true;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ private void ConfigureTls()
+ {
+ this.CheckCertificateRevocationList = true;
+
+ this.ConfigureClientCertificate();
+ this.ConfigureCaCertificateValidation();
+ }
+
+ private void ConfigureClientCertificate()
+ {
+ if (this.clientCertificate == null)
+ {
+ return;
+ }
+
+ this.ClientCertificates.Add(this.clientCertificate);
+ this.ClientCertificateOptions = ClientCertificateOption.Manual;
+ }
+
+ private void ConfigureCaCertificateValidation()
+ {
+ if (this.caCertificate == null)
+ {
+ return;
+ }
+
+ var caCert = this.caCertificate;
+ this.ServerCertificateCustomValidationCallback = (
+ httpRequestMessage,
+ cert,
+ chain,
+ sslPolicyErrors) =>
+ {
+ if (cert == null || chain == null)
+ {
+ return false;
+ }
+
+ return OtlpCertificateManager.ValidateServerCertificate(
+ cert,
+ chain,
+ sslPolicyErrors,
+ caCert);
+ };
+ }
+ }
+}
+
+#endif
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
index 8c91d38d985..cf59c5d3360 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
@@ -75,10 +75,10 @@ internal OtlpExporterOptions(
var timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds);
#if NET
- // If mTLS is configured, create an mTLS-enabled client
+ // If TLS configuration is enabled (mTLS or CA only), create a secure client
if (this.MtlsOptions?.IsEnabled == true)
{
- return OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(
+ return OtlpSecureHttpClientFactory.CreateSecureHttpClient(
this.MtlsOptions,
client => client.Timeout = timeout);
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs
index ebb1f827c77..ae8f7bbadcd 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs
@@ -5,32 +5,48 @@
namespace OpenTelemetry.Exporter;
-internal sealed class OtlpMtlsOptions
+///
+/// Represents mTLS (mutual TLS) configuration options for OTLP exporter.
+/// Extends with client certificate authentication.
+///
+///
+/// mTLS is an authentication system in which both the client and server authenticate each other.
+/// This class provides client certificate configuration for scenarios requiring mutual authentication.
+/// For simple server certificate trust (e.g., self-signed certificates), use directly.
+///
+internal sealed class OtlpMtlsOptions : OtlpTlsOptions
{
- ///
- /// Gets or sets the path to the CA certificate file in PEM format.
- ///
- public string? CaCertificatePath { get; set; }
-
///
/// Gets or sets the path to the client certificate file in PEM format.
///
+ ///
+ /// Corresponds to the OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE environment variable.
+ /// This is used for client authentication in mTLS scenarios.
+ ///
public string? ClientCertificatePath { get; set; }
///
/// Gets or sets the path to the client private key file in PEM format.
///
+ ///
+ /// Corresponds to the OTEL_EXPORTER_OTLP_CLIENT_KEY environment variable.
+ /// Required when the client certificate file does not include the private key.
+ ///
public string? ClientKeyPath { get; set; }
///
- /// Gets or sets a value indicating whether to enable certificate chain validation.
- /// When enabled, the exporter will validate the certificate chain and reject invalid certificates.
+ /// Gets a value indicating whether mTLS (mutual TLS) is enabled.
///
- public bool EnableCertificateChainValidation { get; set; } = true;
+ ///
+ /// Returns true when client certificate is configured for mutual authentication.
+ /// Note: Having only does not constitute mTLS.
+ ///
+ public override bool IsMtlsEnabled =>
+ !string.IsNullOrWhiteSpace(this.ClientCertificatePath);
///
- /// Gets a value indicating whether mTLS is enabled.
- /// mTLS is considered enabled if at least the client certificate path or CA certificate path is provided.
+ /// Gets a value indicating whether any TLS configuration is enabled.
+ /// TLS is considered enabled if at least the client certificate path or CA path is provided.
///
public bool IsEnabled =>
!string.IsNullOrWhiteSpace(this.ClientCertificatePath)
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTlsOptions.cs
new file mode 100644
index 00000000000..1bfbf76f6ab
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTlsOptions.cs
@@ -0,0 +1,50 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+namespace OpenTelemetry.Exporter;
+
+///
+/// Represents TLS configuration options for OTLP exporter.
+/// This class handles server certificate trust for scenarios such as self-signed certificates.
+///
+///
+/// The option enables trust of server certificates
+/// that are not verified by a third-party certificate authority. This is commonly used
+/// when connecting to servers with self-signed certificates.
+///
+internal class OtlpTlsOptions
+{
+ ///
+ /// Gets or sets the path to the CA file in PEM format.
+ ///
+ ///
+ /// This corresponds to the OTEL_EXPORTER_OTLP_CERTIFICATE environment variable.
+ /// Use this when the server has a self-signed certificate or uses a private CA.
+ ///
+ public string? CaCertificatePath { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to enable certificate chain validation.
+ /// When enabled, the exporter will validate the certificate chain and reject invalid certificates.
+ ///
+ public bool EnableCertificateChainValidation { get; set; } = true;
+
+ ///
+ /// Gets a value indicating whether TLS certificate trust is configured.
+ ///
+ public virtual bool IsTlsEnabled =>
+ !string.IsNullOrWhiteSpace(this.CaCertificatePath);
+
+ ///
+ /// Gets a value indicating whether mTLS (mutual TLS) is configured.
+ ///
+ ///
+ /// Returns true only when client certificates are configured for mutual authentication.
+ /// Server certificate trust alone (CaCertificatePath) does not constitute mTLS.
+ ///
+ public virtual bool IsMtlsEnabled => false;
+}
+
+#endif
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpCertificateManagerTests.cs
similarity index 88%
rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs
rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpCertificateManagerTests.cs
index df5b1d811d7..a7eb0a7d4fe 100644
--- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpCertificateManagerTests.cs
@@ -5,7 +5,7 @@
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
-public class OtlpMtlsCertificateManagerTests
+public class OtlpCertificateManagerTests
{
private const string TestCertPem =
@"-----BEGIN CERTIFICATE-----
@@ -39,7 +39,7 @@ INVALID CERTIFICATE DATA
public void LoadClientCertificate_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist()
{
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadClientCertificate(
"/nonexistent/client.crt",
"/nonexistent/client.key"));
@@ -56,7 +56,7 @@ public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFile
try
{
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadClientCertificate(
tempCertFile,
"/nonexistent/client.key"));
@@ -73,7 +73,7 @@ public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFile
public void LoadCaCertificate_ThrowsFileNotFoundException_WhenTrustStoreFileDoesNotExist()
{
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate("/nonexistent/ca.crt"));
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadCaCertificate("/nonexistent/ca.crt"));
Assert.Contains("CA certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("/nonexistent/ca.crt", exception.Message, StringComparison.OrdinalIgnoreCase);
@@ -90,7 +90,7 @@ public void LoadClientCertificate_ThrowsInvalidOperationException_WhenCertificat
try
{
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(tempCertFile, tempKeyFile));
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadClientCertificate(tempCertFile, tempKeyFile));
Assert.Contains(
"Failed to load client certificate",
@@ -113,7 +113,7 @@ public void LoadCaCertificate_ThrowsInvalidOperationException_WhenTrustStoreFile
try
{
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate(tempTrustStoreFile));
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadCaCertificate(tempTrustStoreFile));
Assert.Contains(
"Failed to load CA certificate",
@@ -133,7 +133,7 @@ public void ValidateCertificateChain_DoesNotThrow_WithValidCertificate()
using var cert = CreateSelfSignedCertificate();
// Should not throw for self-signed certificate with proper validation
- var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate");
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateCertificateChain(cert, "test certificate");
// For self-signed certificates, validation may fail, but method should not throw
Assert.True(result || !result); // Just check that it returns a boolean
@@ -146,7 +146,7 @@ public void ValidateCertificateChain_ReturnsResult_WithValidCertificate()
using var cert = CreateSelfSignedCertificate();
// Should return a boolean result
- var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate");
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateCertificateChain(cert, "test certificate");
// The result can be true or false, but the method should not throw
Assert.True(result || !result);
@@ -166,7 +166,7 @@ public void LoadClientCertificate_LoadsFromSeparateFiles()
// Note: We expect this to fail because we're using dummy cert/key content
// but it should not fail due to the method signature
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadClientCertificate(
tempCertFile,
tempKeyFile));
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSecureHttpClientFactoryTests.cs
similarity index 90%
rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs
rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSecureHttpClientFactoryTests.cs
index cb001217ef8..66ff27b481c 100644
--- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSecureHttpClientFactoryTests.cs
@@ -10,7 +10,7 @@
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
-public class OtlpMtlsHttpClientFactoryTests
+public class OtlpSecureHttpClientFactoryTests
{
[Fact]
public void CreateHttpClient_ThrowsInvalidOperationException_WhenMtlsIsDisabled()
@@ -18,7 +18,7 @@ public void CreateHttpClient_ThrowsInvalidOperationException_WhenMtlsIsDisabled(
var options = new OtlpMtlsOptions(); // Disabled by default
Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options));
+ OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options));
}
[Fact]
@@ -27,7 +27,7 @@ public void CreateHttpClient_ThrowsFileNotFoundException_WhenCertificateFileDoes
var options = new OtlpMtlsOptions { ClientCertificatePath = "/nonexistent/client.crt" };
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options));
+ OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options));
Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase);
}
@@ -49,7 +49,7 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro
EnableCertificateChainValidation = false, // Ignore validation for test cert
};
- using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options);
Assert.NotNull(httpClient);
@@ -72,16 +72,16 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro
}
[Fact]
- public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRootCertificatesProvided()
+ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenCaCertificatesProvided()
{
RunWithCryptoSupportCheck(() =>
{
var tempTrustStoreFile = Path.GetTempFileName();
try
{
- // Create a self-signed certificate for testing as trusted root
- using var trustedCert = CreateSelfSignedCertificate();
- File.WriteAllText(tempTrustStoreFile, ExportCertificateWithPrivateKey(trustedCert));
+ // Create a self-signed certificate for testing as CA root
+ using var caCert = CreateSelfSignedCertificate();
+ File.WriteAllText(tempTrustStoreFile, ExportCertificateWithPrivateKey(caCert));
var options = new OtlpMtlsOptions
{
@@ -89,7 +89,7 @@ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRo
EnableCertificateChainValidation = false, // Avoid platform-specific chain build differences
};
- using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options);
Assert.NotNull(httpClient);
@@ -130,7 +130,7 @@ public void CreateHttpClient_ConfiguresServerValidation_WithCaOnly()
EnableCertificateChainValidation = false, // Avoid platform-specific chain build differences
};
- using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options);
var handlerField = typeof(HttpMessageInvoker).GetField(
"_handler",
@@ -171,7 +171,7 @@ public void CreateHttpClient_InvokesServerValidationCallbackAfterFactoryReturns(
EnableCertificateChainValidation = false,
};
- using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options);
var handlerField = typeof(HttpMessageInvoker).GetField(
"_handler",
@@ -212,7 +212,7 @@ public void ValidateServerCertificate_ReturnsTrue_WhenNoSslPolicyErrors()
using var chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
- var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateServerCertificate(
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateServerCertificate(
serverCertificate,
chain,
SslPolicyErrors.None,
@@ -229,7 +229,7 @@ public void ValidateServerCertificate_ReturnsTrue_WithProvidedCa()
using var chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
- var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateServerCertificate(
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateServerCertificate(
serverCertificate,
chain,
SslPolicyErrors.RemoteCertificateChainErrors,
@@ -248,7 +248,7 @@ public void ValidateServerCertificate_ReturnsFalse_WhenCaDoesNotMatch()
using var chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
- var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateServerCertificate(
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateServerCertificate(
serverCertificate,
chain,
SslPolicyErrors.RemoteCertificateChainErrors,
@@ -262,20 +262,20 @@ public void ValidateCertificateChain_ReturnsFalseForExpiredCertificate()
{
using var expiredCertificate = CreateExpiredCertificate();
- var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateCertificateChain(
expiredCertificate,
- OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ClientCertificateType);
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ClientCertificateType);
Assert.False(result);
}
[Fact]
- public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenOptionsIsNull()
+ public void CreateSecureHttpClient_ThrowsArgumentNullException_WhenOptionsIsNull()
{
var exception = Assert.Throws(() =>
- OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(null!));
+ OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(null!));
- Assert.Equal("mtlsOptions", exception.ParamName);
+ Assert.Equal("tlsOptions", exception.ParamName);
}
private static X509Certificate2 CreateSelfSignedCertificate()
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTlsOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTlsOptionsTests.cs
new file mode 100644
index 00000000000..2a4fb828afe
--- /dev/null
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTlsOptionsTests.cs
@@ -0,0 +1,294 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+using System.Net.Security;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
+
+///
+/// Tests for TLS options and secure HTTP client configuration.
+///
+public class OtlpTlsOptionsTests
+{
+ [Fact]
+ public void OtlpTlsOptions_IsTlsEnabled_ReturnsFalse_WhenNoCaCertificatePath()
+ {
+ var options = new OtlpTlsOptions();
+ Assert.False(options.IsTlsEnabled);
+ }
+
+ [Fact]
+ public void OtlpTlsOptions_IsTlsEnabled_ReturnsTrue_WhenCaCertificatePathProvided()
+ {
+ var options = new OtlpTlsOptions { CaCertificatePath = "/path/to/ca.crt" };
+ Assert.True(options.IsTlsEnabled);
+ }
+
+ [Fact]
+ public void OtlpTlsOptions_IsMtlsEnabled_ReturnsFalse_ByDefault()
+ {
+ var options = new OtlpTlsOptions { CaCertificatePath = "/path/to/ca.crt" };
+ Assert.False(options.IsMtlsEnabled);
+ }
+
+ [Fact]
+ public void OtlpMtlsOptions_IsMtlsEnabled_ReturnsTrue_WhenClientCertificateProvided()
+ {
+ var options = new OtlpMtlsOptions { ClientCertificatePath = "/path/to/client.crt" };
+ Assert.True(options.IsMtlsEnabled);
+ }
+
+ [Fact]
+ public void OtlpMtlsOptions_IsMtlsEnabled_ReturnsFalse_WhenOnlyCaCertificateProvided()
+ {
+ // This is the key distinction: CA alone does NOT constitute mTLS
+ var options = new OtlpMtlsOptions { CaCertificatePath = "/path/to/ca.crt" };
+ Assert.False(options.IsMtlsEnabled);
+ Assert.True(options.IsTlsEnabled); // But TLS is still enabled for server cert validation
+ }
+
+ [Fact]
+ public void OtlpSecureHttpClientFactory_CreatesClient_WithCaCertificateOnly()
+ {
+ RunWithCryptoSupportCheck(() =>
+ {
+ var tempCertFile = Path.GetTempFileName();
+ try
+ {
+ using var cert = CreateSelfSignedCertificate();
+ File.WriteAllText(tempCertFile, ExportCertificateWithPrivateKey(cert));
+
+ var options = new OtlpTlsOptions
+ {
+ CaCertificatePath = tempCertFile,
+ EnableCertificateChainValidation = false,
+ };
+
+ using var client = OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options);
+
+ Assert.NotNull(client);
+ }
+ finally
+ {
+ if (File.Exists(tempCertFile))
+ {
+ File.Delete(tempCertFile);
+ }
+ }
+ });
+ }
+
+ [Fact]
+ public void OtlpSecureHttpClientFactory_CreatesClient_WithMtlsClientCertificate()
+ {
+ RunWithCryptoSupportCheck(() =>
+ {
+ var tempCertFile = Path.GetTempFileName();
+ try
+ {
+ using var cert = CreateSelfSignedCertificate();
+ var certBytes = cert.Export(X509ContentType.Pfx);
+ File.WriteAllBytes(tempCertFile, certBytes);
+
+ var options = new OtlpMtlsOptions
+ {
+ ClientCertificatePath = tempCertFile,
+ EnableCertificateChainValidation = false,
+ };
+
+ using var client = OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options);
+
+ Assert.NotNull(client);
+ }
+ finally
+ {
+ if (File.Exists(tempCertFile))
+ {
+ File.Delete(tempCertFile);
+ }
+ }
+ });
+ }
+
+ [Fact]
+ public void OtlpSecureHttpClientFactory_ThrowsArgumentNullException_WhenOptionsIsNull()
+ {
+ Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(null!));
+ }
+
+ [Fact]
+ public void OtlpSecureHttpClientFactory_ThrowsInvalidOperationException_WhenTlsNotEnabled()
+ {
+ var options = new OtlpTlsOptions();
+ Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpSecureHttpClientFactory.CreateSecureHttpClient(options));
+ }
+
+ [Fact]
+ public void OtlpCertificateManager_LoadCaCertificate_ThrowsFileNotFoundException()
+ {
+ Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpCertificateManager.LoadCaCertificate("/nonexistent/cert.pem"));
+ }
+
+ [Fact]
+ public void OtlpCertificateManager_ValidateServerCertificate_ReturnsTrue_WhenNoSslPolicyErrors()
+ {
+ using var caCertificate = CreateCertificateAuthority();
+ using var serverCertificate = CreateServerCertificate(caCertificate);
+ using var chain = new X509Chain();
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateServerCertificate(
+ serverCertificate,
+ chain,
+ SslPolicyErrors.None,
+ caCertificate);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void OtlpCertificateManager_ValidateServerCertificate_ReturnsTrue_WithProvidedTrustedCert()
+ {
+ using var caCertificate = CreateCertificateAuthority();
+ using var serverCertificate = CreateServerCertificate(caCertificate);
+ using var chain = new X509Chain();
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+
+ var result = OpenTelemetryProtocol.Implementation.OtlpCertificateManager.ValidateServerCertificate(
+ serverCertificate,
+ chain,
+ SslPolicyErrors.RemoteCertificateChainErrors,
+ caCertificate);
+
+ Assert.True(result);
+ Assert.Equal(caCertificate.Thumbprint, chain.ChainElements[^1].Certificate.Thumbprint);
+ }
+
+ private static X509Certificate2 CreateSelfSignedCertificate()
+ {
+ using var rsa = RSA.Create(2048);
+ var req = new CertificateRequest(
+ "CN=Test Certificate",
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ var cert = req.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(30));
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
+#else
+#pragma warning disable SYSLIB0057
+ return new X509Certificate2(cert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
+#pragma warning restore SYSLIB0057
+#endif
+ }
+
+ private static X509Certificate2 CreateCertificateAuthority()
+ {
+ using var rsa = RSA.Create(2048);
+ var request = new CertificateRequest(
+ "CN=Test CA",
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
+ request.CertificateExtensions.Add(new X509KeyUsageExtension(
+ X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign,
+ true));
+ request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
+
+ var cert = request.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddYears(1));
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
+#else
+#pragma warning disable SYSLIB0057
+ return new X509Certificate2(cert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
+#pragma warning restore SYSLIB0057
+#endif
+ }
+
+ private static X509Certificate2 CreateServerCertificate(X509Certificate2 issuer)
+ {
+ using var rsa = RSA.Create(2048);
+ var request = new CertificateRequest(
+ "CN=localhost",
+ rsa,
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
+ request.CertificateExtensions.Add(new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment,
+ true));
+ request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
+
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ sanBuilder.AddDnsName("localhost");
+ request.CertificateExtensions.Add(sanBuilder.Build());
+
+ var serialNumber = new byte[16];
+ RandomNumberGenerator.Fill(serialNumber);
+
+ var cert = request.Create(
+ issuer,
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(30),
+ serialNumber);
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
+#else
+#pragma warning disable SYSLIB0057
+ return new X509Certificate2(cert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.Exportable);
+#pragma warning restore SYSLIB0057
+#endif
+ }
+
+ private static string ExportCertificateWithPrivateKey(X509Certificate2 certificate)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine(certificate.ExportCertificatePem().Trim());
+
+ using RSA? privateKey = certificate.GetRSAPrivateKey();
+ if (privateKey != null)
+ {
+ var pkcs8Bytes = privateKey.ExportPkcs8PrivateKey();
+ var privateKeyPem = PemEncoding.Write("PRIVATE KEY", pkcs8Bytes);
+ builder.AppendLine(new string(privateKeyPem).Trim());
+ }
+
+ return builder.ToString();
+ }
+
+ private static void RunWithCryptoSupportCheck(Action testBody)
+ {
+ try
+ {
+ testBody();
+ }
+ catch (PlatformNotSupportedException ex)
+ {
+ Console.WriteLine($"Skipping TLS tests: {ex.Message}");
+ }
+ catch (CryptographicException ex) when (ex.Message.Contains("not supported", StringComparison.OrdinalIgnoreCase))
+ {
+ Console.WriteLine($"Skipping TLS tests: {ex.Message}");
+ }
+ }
+}
+
+#endif