diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
index c4d21d92370..137c1c718c1 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
@@ -240,4 +240,56 @@ void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, str
{
this.InvalidConfigurationValue(key, value);
}
+
+#if NET
+ [Event(26, Message = "{0} loaded successfully from '{1}'.", Level = EventLevel.Informational)]
+ internal void MtlsCertificateLoaded(string certificateType, string filePath) =>
+ this.WriteEvent(26, certificateType, filePath);
+
+ [Event(27, Message = "Failed to load {0} from '{1}'. Error: {2}", Level = EventLevel.Error)]
+ internal void MtlsCertificateLoadFailed(
+ string certificateType,
+ string filePath,
+ string error) => this.WriteEvent(27, certificateType, filePath, error);
+
+ [Event(28, Message = "{0} file not found at path: '{1}'.", Level = EventLevel.Error)]
+ internal void MtlsCertificateFileNotFound(string certificateType, string filePath) =>
+ this.WriteEvent(28, certificateType, filePath);
+
+ [Event(
+ 29,
+ Message = "{0} chain validation failed for certificate '{1}'. Errors: {2}",
+ Level = EventLevel.Error)]
+ internal void MtlsCertificateChainValidationFailed(
+ string certificateType,
+ string subject,
+ string errors) => this.WriteEvent(29, certificateType, subject, errors);
+
+ [Event(
+ 30,
+ Message = "{0} chain validated successfully for certificate '{1}'.",
+ Level = EventLevel.Informational)]
+ internal void MtlsCertificateChainValidated(string certificateType, string subject) =>
+ this.WriteEvent(30, certificateType, subject);
+
+ [Event(
+ 31,
+ Message = "Server certificate validated successfully for '{0}'.",
+ Level = EventLevel.Informational)]
+ internal void MtlsServerCertificateValidated(string subject) => this.WriteEvent(31, subject);
+
+ [Event(
+ 32,
+ Message = "Server certificate validation failed for '{0}'. Errors: {1}",
+ Level = EventLevel.Error)]
+ internal void MtlsServerCertificateValidationFailed(string subject, string errors) =>
+ this.WriteEvent(32, subject, errors);
+
+ [Event(
+ 33,
+ Message = "mTLS configuration enabled. Client certificate: '{0}'.",
+ Level = EventLevel.Informational)]
+ internal void MtlsConfigurationEnabled(string clientCertificateSubject) =>
+ this.WriteEvent(33, clientCertificateSubject);
+#endif
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs
new file mode 100644
index 00000000000..4f358142a74
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs
@@ -0,0 +1,297 @@
+// 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;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
+
+///
+/// Manages certificate loading, validation, and security checks for mTLS connections.
+///
+internal static class OtlpMtlsCertificateManager
+{
+ internal const string CaCertificateType = "CA certificate";
+ internal const string ClientCertificateType = "Client certificate";
+ internal const string ClientPrivateKeyType = "Client private key";
+
+ ///
+ /// Loads a CA certificate from a PEM file.
+ ///
+ /// Path to the CA certificate file.
+ /// The loaded CA certificate.
+ /// Thrown when the certificate file is not found.
+ /// Thrown when the certificate cannot be loaded.
+ public static X509Certificate2 LoadCaCertificate(string caCertificatePath)
+ {
+ ValidateFileExists(caCertificatePath, CaCertificateType);
+
+ try
+ {
+ var caCertificate = X509Certificate2.CreateFromPemFile(caCertificatePath);
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded(
+ CaCertificateType,
+ caCertificatePath);
+
+ return caCertificate;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed(
+ CaCertificateType,
+ caCertificatePath,
+ ex.Message);
+ throw new InvalidOperationException(
+ $"Failed to load CA certificate from '{caCertificatePath}': {ex.Message}",
+ ex);
+ }
+ }
+
+ ///
+ /// Loads a client certificate from a single file (e.g., PKCS#12 format) or from separate certificate and key files.
+ ///
+ /// Path to the client certificate file.
+ /// Path to the client private key file. Can be null for single-file certificates.
+ /// The loaded client certificate with private key.
+ /// Thrown when the certificate file is not found.
+ /// Thrown when the certificate cannot be loaded.
+ /// Thrown when clientKeyPath is not null for single-file certificate loading.
+ public static X509Certificate2 LoadClientCertificate(
+ string clientCertificatePath,
+ string? clientKeyPath)
+ {
+ if (clientKeyPath == null)
+ {
+ // Load certificate from a single file (e.g., PKCS#12 format)
+ ValidateFileExists(clientCertificatePath, ClientCertificateType);
+
+ try
+ {
+ X509Certificate2 clientCertificate;
+
+ // Try to load as PKCS#12 first, then as PEM
+ try
+ {
+#if NET9_0_OR_GREATER
+ clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, (string?)null);
+#else
+ clientCertificate = new X509Certificate2(clientCertificatePath);
+#endif
+ }
+ catch (Exception ex) when (ex is CryptographicException || ex is InvalidDataException || ex is FormatException)
+ {
+ // If PKCS#12 fails, try PEM format
+ clientCertificate = X509Certificate2.CreateFromPemFile(clientCertificatePath);
+ }
+
+ if (!clientCertificate.HasPrivateKey)
+ {
+ throw new InvalidOperationException(
+ "Client certificate does not have an associated private key.");
+ }
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded(
+ ClientCertificateType,
+ clientCertificatePath);
+
+ return clientCertificate;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed(
+ ClientCertificateType,
+ clientCertificatePath,
+ ex.Message);
+ throw new InvalidOperationException(
+ $"Failed to load client certificate from '{clientCertificatePath}': {ex.Message}",
+ ex);
+ }
+ }
+
+ // Load certificate and key from separate files
+ ValidateFileExists(clientCertificatePath, ClientCertificateType);
+ ValidateFileExists(clientKeyPath, ClientPrivateKeyType);
+
+ try
+ {
+ X509Certificate2 clientCertificate = X509Certificate2.CreateFromPemFile(
+ clientCertificatePath,
+ clientKeyPath);
+
+ if (!clientCertificate.HasPrivateKey)
+ {
+ throw new InvalidOperationException(
+ "Client certificate does not have an associated private key.");
+ }
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded(
+ ClientCertificateType,
+ clientCertificatePath);
+
+ return clientCertificate;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed(
+ ClientCertificateType,
+ clientCertificatePath,
+ ex.Message);
+ throw new InvalidOperationException(
+ $"Failed to load client certificate from '{clientCertificatePath}' and key from '{clientKeyPath}': {ex.Message}",
+ ex);
+ }
+ }
+
+ ///
+ /// Validates the certificate chain for a given certificate.
+ ///
+ /// The certificate to validate.
+ /// Type description for logging (e.g., "Client certificate").
+ /// True if the certificate chain is valid; otherwise, false.
+ public static bool ValidateCertificateChain(
+ X509Certificate2 certificate,
+ string certificateType)
+ {
+ try
+ {
+ using var chain = new X509Chain();
+
+ // Configure chain policy
+ chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
+ chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
+
+ bool isValid = chain.Build(certificate);
+
+ if (!isValid)
+ {
+ var errors = chain
+ .ChainStatus.Where(status => status.Status != X509ChainStatusFlags.NoError)
+ .Select(status => $"{status.Status}: {status.StatusInformation}")
+ .ToArray();
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateChainValidationFailed(
+ certificateType,
+ certificate.Subject,
+ string.Join("; ", errors));
+
+ // Check if certificate is expired - this should throw an exception
+ bool isExpired = chain.ChainStatus.Any(status =>
+ status.Status == X509ChainStatusFlags.NotTimeValid ||
+ status.Status == X509ChainStatusFlags.NotTimeNested);
+
+ if (isExpired)
+ {
+ throw new InvalidOperationException(
+ $"Certificate chain validation failed for {certificateType}: Certificate is expired. " +
+ $"Errors: {string.Join("; ", errors)}");
+ }
+
+ return false;
+ }
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateChainValidated(
+ certificateType,
+ certificate.Subject);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateChainValidationFailed(
+ certificateType,
+ certificate.Subject,
+ ex.Message);
+
+ return false;
+ }
+ }
+
+ ///
+ /// Validates a server certificate against the provided CA certificate.
+ ///
+ /// The server certificate to validate.
+ /// The certificate chain.
+ /// The SSL policy errors.
+ /// The CA certificate to validate against.
+ /// True if the certificate is valid; otherwise, false.
+ internal static bool ValidateServerCertificate(
+ X509Certificate2 serverCert,
+ X509Chain chain,
+ SslPolicyErrors sslPolicyErrors,
+ X509Certificate2 caCertificate)
+ {
+ try
+ {
+ // If there are no SSL policy errors, accept the certificate
+ if (sslPolicyErrors == SslPolicyErrors.None)
+ {
+ return true;
+ }
+
+ // If the only error is an untrusted root, validate against our CA
+ if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
+ {
+ // Add our CA certificate to the chain
+ chain.ChainPolicy.ExtraStore.Add(caCertificate);
+ chain.ChainPolicy.VerificationFlags =
+ X509VerificationFlags.AllowUnknownCertificateAuthority;
+
+ bool isValid = chain.Build(serverCert);
+
+ if (isValid)
+ {
+ // Verify that the chain terminates with our CA
+ var rootCert = chain.ChainElements[^1].Certificate;
+ if (
+ string.Equals(
+ rootCert.Thumbprint,
+ caCertificate.Thumbprint,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidated(
+ serverCert.Subject);
+ return true;
+ }
+ }
+ }
+
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed(
+ serverCert.Subject,
+ sslPolicyErrors.ToString());
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed(
+ serverCert.Subject,
+ ex.Message);
+
+ return false;
+ }
+ }
+
+ private static void ValidateFileExists(string filePath, string fileType)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException(
+ $"{fileType} path cannot be null or empty.",
+ nameof(filePath));
+ }
+
+ if (!File.Exists(filePath))
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateFileNotFound(
+ fileType,
+ filePath);
+ throw new FileNotFoundException($"{fileType} file not found at path: {filePath}", filePath);
+ }
+ }
+}
+
+#endif
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs
new file mode 100644
index 00000000000..a0f5169b0aa
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs
@@ -0,0 +1,135 @@
+// 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.
+ public static HttpClient CreateMtlsHttpClient(
+ OtlpMtlsOptions mtlsOptions,
+ Action? configureClient = null)
+ {
+ ArgumentNullException.ThrowIfNull(mtlsOptions);
+
+ if (!mtlsOptions.IsEnabled)
+ {
+ var client = new HttpClient();
+ configureClient?.Invoke(client);
+ return client;
+ }
+
+ 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 HttpClientHandler { CheckCertificateRevocationList = true };
+#pragma warning restore CA2000
+
+ // Add client certificate if available
+ if (clientCertificate != null)
+ {
+ handler.ClientCertificates.Add(clientCertificate);
+ handler.ClientCertificateOptions = ClientCertificateOption.Manual;
+ }
+
+ // Set up server certificate validation
+ if (caCertificate != null)
+ {
+ handler.ServerCertificateCustomValidationCallback = (
+ httpRequestMessage,
+ cert,
+ chain,
+ sslPolicyErrors) =>
+ {
+ if (cert == null || chain == null)
+ {
+ return false;
+ }
+
+ return OtlpMtlsCertificateManager.ValidateServerCertificate(
+ cert, chain, sslPolicyErrors, caCertificate);
+ };
+ }
+
+ 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.ExportMethodException(ex);
+ throw;
+ }
+ finally
+ {
+ // Dispose certificates as they are no longer needed after being added to the handler
+ caCertificate?.Dispose();
+ clientCertificate?.Dispose();
+ }
+ }
+}
+
+#endif
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs
index 3bc62218b3f..8e8fd497b0d 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs
@@ -31,4 +31,9 @@ internal static class OtlpSpecConfigDefinitions
public const string TracesHeadersEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_HEADERS";
public const string TracesTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT";
public const string TracesProtocolEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
+
+ // mTLS certificate environment variables
+ public const string CertificateEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE";
+ public const string ClientKeyEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_KEY";
+ public const string ClientCertificateEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE";
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
index 91ebfdbd3e1..b3f67444bf0 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
@@ -55,9 +55,9 @@ public OtlpExporterOptions()
internal OtlpExporterOptions(
OtlpExporterOptionsConfigurationType configurationType)
: this(
- configuration: new ConfigurationBuilder().AddEnvironmentVariables().Build(),
- configurationType,
- defaultBatchOptions: new())
+ configuration: new ConfigurationBuilder().AddEnvironmentVariables().Build(),
+ configurationType,
+ defaultBatchOptions: new())
{
}
@@ -72,9 +72,21 @@ internal OtlpExporterOptions(
this.DefaultHttpClientFactory = () =>
{
+ var timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds);
+
+#if NET
+ // If mTLS is configured, create an mTLS-enabled client
+ if (this.MtlsOptions?.IsEnabled == true)
+ {
+ return OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(
+ this.MtlsOptions,
+ client => client.Timeout = timeout);
+ }
+#endif
+
return new HttpClient
{
- Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds),
+ Timeout = timeout,
};
};
@@ -158,6 +170,10 @@ public Func HttpClientFactory
///
internal bool AppendSignalPathToEndpoint { get; private set; } = true;
+#if NET
+ internal OtlpMtlsOptions? MtlsOptions { get; set; }
+#endif
+
internal bool HasData
=> this.protocol.HasValue
|| this.endpoint != null
@@ -167,8 +183,7 @@ internal bool HasData
internal static OtlpExporterOptions CreateOtlpExporterOptions(
IServiceProvider serviceProvider,
IConfiguration configuration,
- string name)
- => new(
+ string name) => new(
configuration,
OtlpExporterOptionsConfigurationType.Default,
serviceProvider.GetRequiredService>().Get(name));
@@ -188,10 +203,10 @@ internal void ApplyConfigurationUsingSpecificationEnvVars(
}
if (configuration.TryGetValue(
- OpenTelemetryProtocolExporterEventSource.Log,
- protocolEnvVarKey,
- OtlpExportProtocolParser.TryParse,
- out var protocol))
+ OpenTelemetryProtocolExporterEventSource.Log,
+ protocolEnvVarKey,
+ OtlpExportProtocolParser.TryParse,
+ out var protocol))
{
this.Protocol = protocol;
}
@@ -286,5 +301,38 @@ private void ApplyConfiguration(
{
throw new NotSupportedException($"OtlpExporterOptionsConfigurationType '{configurationType}' is not supported.");
}
+
+#if NET
+ // Apply mTLS configuration from environment variables
+ this.ApplyMtlsConfiguration(configuration);
+#endif
}
+
+#if NET
+ private void ApplyMtlsConfiguration(IConfiguration configuration)
+ {
+ Debug.Assert(configuration != null, "configuration was null");
+
+ // Check and apply CA certificate path from environment variable
+ if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateEnvVarName, out var caCertPath))
+ {
+ this.MtlsOptions ??= new();
+ this.MtlsOptions.CaCertificatePath = caCertPath;
+ }
+
+ // Check and apply client certificate path from environment variable
+ if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientCertificateEnvVarName, out var clientCertPath))
+ {
+ this.MtlsOptions ??= new();
+ this.MtlsOptions.ClientCertificatePath = clientCertPath;
+ }
+
+ // Check and apply client key path from environment variable
+ if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientKeyEnvVarName, out var clientKeyPath))
+ {
+ this.MtlsOptions ??= new();
+ this.MtlsOptions.ClientKeyPath = clientKeyPath;
+ }
+ }
+#endif
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs
new file mode 100644
index 00000000000..dc2feaa8e8e
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs
@@ -0,0 +1,38 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+namespace OpenTelemetry.Exporter;
+
+internal sealed class OtlpMtlsOptions
+{
+ ///
+ /// 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.
+ ///
+ public string? ClientCertificatePath { get; set; }
+
+ ///
+ /// Gets or sets the path to the client private key file in PEM format.
+ ///
+ 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.
+ ///
+ public bool EnableCertificateChainValidation { get; set; } = true;
+
+ ///
+ /// Gets a value indicating whether mTLS is enabled.
+ /// mTLS is considered enabled if at least the client certificate path is provided.
+ ///
+ public bool IsEnabled => !string.IsNullOrWhiteSpace(this.ClientCertificatePath);
+}
+
+#endif
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md
index e12ba68ff22..b358a7b9edc 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md
@@ -450,6 +450,15 @@ or reader
| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `Protocol` (`grpc` or `http/protobuf`)|
+ The following environment variables can be used to configure mTLS
+ (mutual TLS) authentication (.NET 8.0+ only):
+
+ | Environment variable | `OtlpMtlsOptions` property | Description |
+ | -------------------------------------------------| ----------------------------- | ------------------------------------- |
+ | `OTEL_EXPORTER_OTLP_CERTIFICATE` | `CaCertificatePath` | Path to CA certificate file (PEM) |
+ | `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | `ClientCertificatePath` | Path to client certificate file (PEM) |
+ | `OTEL_EXPORTER_OTLP_CLIENT_KEY` | `ClientKeyPath` | Path to client private key file (PEM) |
+
* Logs:
The following environment variables can be used to override the default values
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs
index 9536d283cf5..52271cf5b34 100644
--- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs
@@ -263,4 +263,107 @@ public void OtlpExporterOptions_ApplyDefaultsTest()
Assert.NotEqual(defaultOptionsWithData.TimeoutMilliseconds, targetOptionsWithData.TimeoutMilliseconds);
Assert.NotEqual(defaultOptionsWithData.HttpClientFactory, targetOptionsWithData.HttpClientFactory);
}
+
+#if NET
+ [Fact]
+ public void OtlpExporterOptions_MtlsEnvironmentVariables()
+ {
+ // Test CA certificate environment variable
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CERTIFICATE", "/path/to/ca.crt");
+
+ try
+ {
+ var options = new OtlpExporterOptions();
+
+ Assert.NotNull(options.MtlsOptions);
+ Assert.Equal("/path/to/ca.crt", options.MtlsOptions.CaCertificatePath);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CERTIFICATE", null);
+ }
+ }
+
+ [Fact]
+ public void OtlpExporterOptions_MtlsEnvironmentVariables_ClientCertificate()
+ {
+ // Test client certificate and key environment variables
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client.crt");
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/client.key");
+
+ try
+ {
+ var options = new OtlpExporterOptions();
+
+ Assert.NotNull(options.MtlsOptions);
+ Assert.Equal("/path/to/client.crt", options.MtlsOptions.ClientCertificatePath);
+ Assert.Equal("/path/to/client.key", options.MtlsOptions.ClientKeyPath);
+ Assert.True(options.MtlsOptions.IsEnabled);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", null);
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", null);
+ }
+ }
+
+ [Fact]
+ public void OtlpExporterOptions_MtlsEnvironmentVariables_AllCertificates()
+ {
+ // Test all mTLS environment variables together
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CERTIFICATE", "/path/to/ca.crt");
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client.crt");
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/client.key");
+
+ try
+ {
+ var options = new OtlpExporterOptions();
+
+ Assert.NotNull(options.MtlsOptions);
+ Assert.Equal("/path/to/ca.crt", options.MtlsOptions.CaCertificatePath);
+ Assert.Equal("/path/to/client.crt", options.MtlsOptions.ClientCertificatePath);
+ Assert.Equal("/path/to/client.key", options.MtlsOptions.ClientKeyPath);
+ Assert.True(options.MtlsOptions.IsEnabled);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CERTIFICATE", null);
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", null);
+ Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", null);
+ }
+ }
+
+ [Fact]
+ public void OtlpExporterOptions_MtlsEnvironmentVariables_NoEnvironmentVariables()
+ {
+ // Ensure no mTLS options are set when no environment variables are present
+ var options = new OtlpExporterOptions();
+
+ Assert.Null(options.MtlsOptions);
+ }
+
+ [Fact]
+ public void OtlpExporterOptions_MtlsEnvironmentVariables_UsingIConfiguration()
+ {
+ // Test using IConfiguration instead of environment variables
+ var values = new Dictionary
+ {
+ ["OTEL_EXPORTER_OTLP_CERTIFICATE"] = "/config/path/to/ca.crt",
+ ["OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"] = "/config/path/to/client.crt",
+ ["OTEL_EXPORTER_OTLP_CLIENT_KEY"] = "/config/path/to/client.key",
+ };
+
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(values)
+ .Build();
+
+ var options = new OtlpExporterOptions(configuration, OtlpExporterOptionsConfigurationType.Default, new());
+
+ Assert.NotNull(options.MtlsOptions);
+ Assert.Equal("/config/path/to/ca.crt", options.MtlsOptions.CaCertificatePath);
+ Assert.Equal("/config/path/to/client.crt", options.MtlsOptions.ClientCertificatePath);
+ Assert.Equal("/config/path/to/client.key", options.MtlsOptions.ClientKeyPath);
+ Assert.True(options.MtlsOptions.IsEnabled);
+ }
+#endif
}
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs
new file mode 100644
index 00000000000..d3fac9900a7
--- /dev/null
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs
@@ -0,0 +1,212 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+using Xunit;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
+
+public class OtlpMtlsCertificateManagerTests
+{
+ private const string TestCertPem =
+ @"-----BEGIN CERTIFICATE-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1234567890ABCDEFGHIJ
+KLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890A
+BCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123
+4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV
+WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO
+PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG
+HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
+ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123
+4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV
+WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO
+PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG
+HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
+ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123
+4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV
+WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO
+PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG
+HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
+ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123
+4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCD
+-----END CERTIFICATE-----";
+
+ [Fact]
+ public void LoadClientCertificate_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist()
+ {
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(
+ "/nonexistent/client.crt",
+ "/nonexistent/client.key"));
+
+ Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("/nonexistent/client.crt", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFileDoesNotExist()
+ {
+ var tempCertFile = Path.GetTempFileName();
+ File.WriteAllText(tempCertFile, TestCertPem);
+
+ try
+ {
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(
+ tempCertFile,
+ "/nonexistent/client.key"));
+
+ Assert.Contains("Private key file not found", exception.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("/nonexistent/client.key", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+ finally
+ {
+ File.Delete(tempCertFile);
+ }
+ }
+
+ [Fact]
+ public void LoadCaCertificate_ThrowsFileNotFoundException_WhenTrustStoreFileDoesNotExist()
+ {
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate("/nonexistent/ca.crt"));
+
+ Assert.Contains("CA certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("/nonexistent/ca.crt", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void LoadClientCertificate_ThrowsInvalidOperationException_WhenCertificateFileIsEmpty()
+ {
+ var tempCertFile = Path.GetTempFileName();
+ var tempKeyFile = Path.GetTempFileName();
+ File.WriteAllText(tempCertFile, string.Empty);
+ File.WriteAllText(tempKeyFile, string.Empty);
+
+ try
+ {
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(tempCertFile, tempKeyFile));
+
+ Assert.Contains(
+ "Failed to load client certificate",
+ exception.Message,
+ StringComparison.OrdinalIgnoreCase);
+ }
+ finally
+ {
+ File.Delete(tempCertFile);
+ File.Delete(tempKeyFile);
+ }
+ }
+
+ [Fact]
+ public void LoadCaCertificate_ThrowsInvalidOperationException_WhenTrustStoreFileIsEmpty()
+ {
+ var tempTrustStoreFile = Path.GetTempFileName();
+ File.WriteAllText(tempTrustStoreFile, string.Empty);
+
+ try
+ {
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate(tempTrustStoreFile));
+
+ Assert.Contains(
+ "Failed to load CA certificate",
+ exception.Message,
+ StringComparison.OrdinalIgnoreCase);
+ }
+ finally
+ {
+ File.Delete(tempTrustStoreFile);
+ }
+ }
+
+ [Fact]
+ public void ValidateCertificateChain_DoesNotThrow_WithValidCertificate()
+ {
+ // Create a self-signed certificate for testing
+ using var cert = CreateSelfSignedCertificate();
+
+ // Should not throw for self-signed certificate with proper validation
+ var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.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
+ }
+
+ [Fact]
+ public void ValidateCertificateChain_ReturnsResult_WithValidCertificate()
+ {
+ // Create a valid certificate for testing
+ using var cert = CreateSelfSignedCertificate();
+
+ // Should return a boolean result
+ var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate");
+
+ // The result can be true or false, but the method should not throw
+ Assert.True(result || !result);
+ }
+
+ [Fact]
+ public void LoadClientCertificate_LoadsFromSeparateFiles()
+ {
+ var tempCertFile = Path.GetTempFileName();
+ var tempKeyFile = Path.GetTempFileName();
+ File.WriteAllText(tempCertFile, TestCertPem);
+ File.WriteAllText(tempKeyFile, "test-key-content");
+
+ try
+ {
+ // This test verifies that the method loads from separate files
+ // 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(
+ tempCertFile,
+ tempKeyFile));
+
+ // The exception should be about certificate loading, not method signature
+ Assert.Contains("Failed to load client certificate", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+ finally
+ {
+ File.Delete(tempCertFile);
+ File.Delete(tempKeyFile);
+ }
+ }
+
+ private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSignedCertificate()
+ {
+ using var rsa = System.Security.Cryptography.RSA.Create(2048);
+ var req = new System.Security.Cryptography.X509Certificates.CertificateRequest(
+ "CN=Test Certificate",
+ rsa,
+ System.Security.Cryptography.HashAlgorithmName.SHA256,
+ System.Security.Cryptography.RSASignaturePadding.Pkcs1);
+
+ var cert = req.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(30));
+ return cert;
+ }
+
+ private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateExpiredCertificate()
+ {
+ using var rsa = System.Security.Cryptography.RSA.Create(2048);
+ var req = new System.Security.Cryptography.X509Certificates.CertificateRequest(
+ "CN=Expired Test Certificate",
+ rsa,
+ System.Security.Cryptography.HashAlgorithmName.SHA256,
+ System.Security.Cryptography.RSASignaturePadding.Pkcs1);
+
+ // Create a certificate that expired yesterday
+ var cert = req.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-30),
+ DateTimeOffset.UtcNow.AddDays(-1));
+ return cert;
+ }
+}
+
+#endif
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs
new file mode 100644
index 00000000000..89fdfed9841
--- /dev/null
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs
@@ -0,0 +1,138 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+using Xunit;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
+
+public class OtlpMtlsHttpClientFactoryTests
+{
+ [Fact]
+ public void CreateHttpClient_ReturnsHttpClient_WhenMtlsIsDisabled()
+ {
+ var options = new OtlpMtlsOptions(); // Disabled by default
+
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+
+ Assert.NotNull(httpClient);
+ Assert.IsType(httpClient);
+ }
+
+ [Fact]
+ public void CreateHttpClient_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist()
+ {
+ var options = new OtlpMtlsOptions { ClientCertificatePath = "/nonexistent/client.crt" };
+
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options));
+
+ Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificateProvided()
+ {
+ var tempCertFile = Path.GetTempFileName();
+ try
+ {
+ // Create a self-signed certificate for testing
+ using var cert = CreateSelfSignedCertificate();
+ var certBytes = cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx);
+ File.WriteAllBytes(tempCertFile, certBytes);
+
+ var options = new OtlpMtlsOptions
+ {
+ ClientCertificatePath = tempCertFile,
+ EnableCertificateChainValidation = false, // Ignore validation for test cert
+ };
+
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+
+ Assert.NotNull(httpClient);
+
+ // Verify the HttpClientHandler has client certificates configured
+ var handlerField = typeof(HttpClient).GetField(
+ "_handler",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (handlerField?.GetValue(httpClient) is HttpClientHandler handler)
+ {
+ Assert.NotEmpty(handler.ClientCertificates);
+ }
+ }
+ finally
+ {
+ if (File.Exists(tempCertFile))
+ {
+ File.Delete(tempCertFile);
+ }
+ }
+ }
+
+ [Fact]
+ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRootCertificatesProvided()
+ {
+ var tempTrustStoreFile = Path.GetTempFileName();
+ try
+ {
+ // Create a self-signed certificate for testing as trusted root
+ using var trustedCert = CreateSelfSignedCertificate();
+ var trustedCertPem = Convert.ToBase64String(trustedCert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Cert));
+ var pemContent =
+ $"-----BEGIN CERTIFICATE-----\n{trustedCertPem}\n-----END CERTIFICATE-----";
+ File.WriteAllText(tempTrustStoreFile, pemContent);
+
+ var options = new OtlpMtlsOptions
+ {
+ CaCertificatePath = tempTrustStoreFile,
+ };
+
+ using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options);
+
+ Assert.NotNull(httpClient);
+
+ // Verify the HttpClientHandler has server certificate validation configured
+ var handlerField = typeof(HttpClient).GetField(
+ "_handler",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (handlerField?.GetValue(httpClient) is HttpClientHandler handler)
+ {
+ Assert.NotNull(handler.ServerCertificateCustomValidationCallback);
+ }
+ }
+ finally
+ {
+ if (File.Exists(tempTrustStoreFile))
+ {
+ File.Delete(tempTrustStoreFile);
+ }
+ }
+ }
+
+ [Fact]
+ public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenOptionsIsNull()
+ {
+ var exception = Assert.Throws(() =>
+ OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(null!));
+
+ Assert.Equal("mtlsOptions", exception.ParamName);
+ }
+
+ private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSignedCertificate()
+ {
+ using var rsa = System.Security.Cryptography.RSA.Create(2048);
+ var req = new System.Security.Cryptography.X509Certificates.CertificateRequest(
+ "CN=Test Certificate",
+ rsa,
+ System.Security.Cryptography.HashAlgorithmName.SHA256,
+ System.Security.Cryptography.RSASignaturePadding.Pkcs1);
+
+ var cert = req.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(30));
+ return cert;
+ }
+}
+
+#endif
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs
new file mode 100644
index 00000000000..e5d9128fe4b
--- /dev/null
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs
@@ -0,0 +1,66 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+using Xunit;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
+
+public class OtlpMtlsOptionsTests
+{
+ [Fact]
+ public void DefaultValues_AreValid()
+ {
+ var options = new OtlpMtlsOptions();
+
+ Assert.Null(options.ClientCertificatePath);
+ Assert.Null(options.ClientKeyPath);
+ Assert.Null(options.CaCertificatePath);
+ Assert.True(options.EnableCertificateChainValidation);
+ Assert.False(options.IsEnabled);
+ }
+
+ [Fact]
+ public void Properties_CanBeSet()
+ {
+ var options = new OtlpMtlsOptions
+ {
+ ClientCertificatePath = "/path/to/client.crt",
+ ClientKeyPath = "/path/to/client.key",
+ CaCertificatePath = "/path/to/ca.crt",
+ EnableCertificateChainValidation = false,
+ };
+
+ Assert.Equal("/path/to/client.crt", options.ClientCertificatePath);
+ Assert.Equal("/path/to/client.key", options.ClientKeyPath);
+ Assert.Equal("/path/to/ca.crt", options.CaCertificatePath);
+ Assert.False(options.EnableCertificateChainValidation);
+ Assert.True(options.IsEnabled);
+ }
+
+ [Fact]
+ public void IsEnabled_ReturnsFalse_WhenNoClientCertificateProvided()
+ {
+ var options = new OtlpMtlsOptions();
+ Assert.False(options.IsEnabled);
+ }
+
+ [Fact]
+ public void IsEnabled_ReturnsTrue_WhenClientCertificateFilePathProvided()
+ {
+ var options = new OtlpMtlsOptions { ClientCertificatePath = "/path/to/client.crt" };
+ Assert.True(options.IsEnabled);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void IsEnabled_ReturnsFalse_WhenClientCertificateFilePathIsEmpty(string filePath)
+ {
+ var options = new OtlpMtlsOptions { ClientCertificatePath = filePath };
+ Assert.False(options.IsEnabled);
+ }
+}
+
+#endif
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs
new file mode 100644
index 00000000000..11c9b5b33d0
--- /dev/null
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs
@@ -0,0 +1,32 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+#if NET
+
+using Xunit;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;
+
+public class OtlpSpecConfigDefinitionsTests
+{
+ [Fact]
+ public void AllEnvironmentVariableNames_AreUnique()
+ {
+ var envVars = new[]
+ {
+ OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName,
+ OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName,
+ OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName,
+ OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName,
+ OtlpSpecConfigDefinitions.CertificateEnvVarName,
+ OtlpSpecConfigDefinitions.ClientKeyEnvVarName,
+ OtlpSpecConfigDefinitions.ClientCertificateEnvVarName,
+ };
+
+ var uniqueVars = envVars.Distinct().ToArray();
+
+ Assert.Equal(envVars.Length, uniqueVars.Length);
+ }
+}
+
+#endif