Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;

/// <summary>
/// Manages certificate loading, validation, and security checks for mTLS connections.
/// Manages certificate loading, validation, and security checks for TLS connections.
/// </summary>
/// <remarks>
/// This class provides functionality for both simple server certificate trust
/// (for self-signed certificates) and mTLS client authentication scenarios.
/// </remarks>
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";

Expand Down Expand Up @@ -218,6 +222,10 @@ public static bool ValidateCertificateChain(
/// <param name="sslPolicyErrors">The SSL policy errors.</param>
/// <param name="caCertificate">The CA certificate to validate against.</param>
/// <returns>True if the certificate is valid; otherwise, false.</returns>
/// <remarks>
/// This method is used to validate server certificates against a CA.
/// Common use case: connecting to a server with a self-signed certificate.
/// </remarks>
internal static bool ValidateServerCertificate(
Comment thread
sandy2008 marked this conversation as resolved.
X509Certificate2 serverCert,
X509Chain chain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,66 +8,60 @@
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;

/// <summary>
/// Factory for creating HttpClient instances configured with mTLS settings.
/// Factory for creating HttpClient instances configured with TLS settings.
/// </summary>
internal static class OtlpSecureHttpClientFactory
{
/// <summary>
/// Creates an HttpClient configured with mTLS settings.
/// Creates an HttpClient configured with TLS settings based on the provided options.
/// </summary>
/// <param name="mtlsOptions">The mTLS configuration options.</param>
/// <param name="tlsOptions">The TLS configuration options.</param>
/// <param name="configureClient">Optional action to configure the client.</param>
/// <returns>An HttpClient configured for mTLS.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="mtlsOptions"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when mTLS is not enabled.</exception>
/// <returns>An HttpClient configured for secure communication.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="tlsOptions"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when TLS is not enabled.</exception>
public static HttpClient CreateSecureHttpClient(
OtlpMtlsOptions mtlsOptions,
OtlpTlsOptions tlsOptions,
Action<HttpClient>? configureClient = null)
{
ArgumentNullException.ThrowIfNull(mtlsOptions);
ArgumentNullException.ThrowIfNull(tlsOptions);

if (!mtlsOptions.IsEnabled)
if (!tlsOptions.IsTlsEnabled && !tlsOptions.IsMtlsEnabled)
{
throw new InvalidOperationException("mTLS options must include a client or CA certificate path.");
throw new InvalidOperationException(
"TLS options must include at least a CA path or client certificate path.");
}

HttpClientHandler? handler = null;
X509Certificate2? caCertificate = null;
X509Certificate2? clientCertificate = null;
TlsHttpClientHandler? handler = null;

try
{
// Load certificates
if (!string.IsNullOrEmpty(mtlsOptions.CaCertificatePath))
if (!string.IsNullOrEmpty(tlsOptions.CaCertificatePath))
{
caCertificate = OtlpCertificateManager.LoadCaCertificate(
mtlsOptions.CaCertificatePath);
tlsOptions.CaCertificatePath);

if (mtlsOptions.EnableCertificateChainValidation)
if (tlsOptions.EnableCertificateChainValidation)
{
OtlpCertificateManager.ValidateCertificateChain(
caCertificate,
OtlpCertificateManager.CaCertificateType);
}
}

if (!string.IsNullOrEmpty(mtlsOptions.ClientCertificatePath))
if (tlsOptions is OtlpMtlsOptions mtlsOptions && mtlsOptions.IsMtlsEnabled)
{
if (string.IsNullOrEmpty(mtlsOptions.ClientKeyPath))
{
// Load certificate without separate key file (e.g., PKCS#12 format)
clientCertificate = OtlpCertificateManager.LoadClientCertificate(
mtlsOptions.ClientCertificatePath,
null);
}
else
{
clientCertificate = OtlpCertificateManager.LoadClientCertificate(
mtlsOptions.ClientCertificatePath,
clientCertificate = string.IsNullOrEmpty(mtlsOptions.ClientKeyPath)
? OtlpCertificateManager.LoadClientCertificate(
mtlsOptions.ClientCertificatePath!,
null)
: OtlpCertificateManager.LoadClientCertificate(
mtlsOptions.ClientCertificatePath!,
mtlsOptions.ClientKeyPath);
}

if (mtlsOptions.EnableCertificateChainValidation)
if (tlsOptions.EnableCertificateChainValidation)
{
OtlpCertificateManager.ValidateCertificateChain(
clientCertificate,
Expand All @@ -77,89 +71,134 @@ public static HttpClient CreateSecureHttpClient(
OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled(
clientCertificate.Subject);
}
else if (caCertificate != null)
{
OpenTelemetryProtocolExporterEventSource.Log.CaCertificateConfigured(
caCertificate.Subject);
}

// Create HttpClientHandler with mTLS configuration
// Create HttpClientHandler and apply TLS configuration
#pragma warning disable CA2000 // Dispose objects before losing scope - HttpClientHandler is disposed by HttpClient
handler = new MtlsHttpClientHandler(clientCertificate, caCertificate);
handler = new TlsHttpClientHandler(caCertificate, clientCertificate);
#pragma warning restore CA2000
handler.CheckCertificateRevocationList = true;

// Handler now owns the certificates and will dispose them when disposed.
// 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)
{
// Dispose handler if something went wrong
handler?.Dispose();

OpenTelemetryProtocolExporterEventSource.Log.MtlsHttpClientCreationFailed(ex);
OpenTelemetryProtocolExporterEventSource.Log.SecureHttpClientCreationFailed(ex);
throw;
}
finally
{
// Dispose certificates as they are no longer needed after being added to the handler
caCertificate?.Dispose();
Comment thread
sandy2008 marked this conversation as resolved.
clientCertificate?.Dispose();
}
}

private sealed class MtlsHttpClientHandler : HttpClientHandler
/// <summary>
/// Creates an HttpClient configured with mTLS settings.
/// </summary>
/// <param name="mtlsOptions">The mTLS configuration options.</param>
/// <param name="configureClient">Optional action to configure the client.</param>
/// <returns>An HttpClient configured for mTLS.</returns>
/// <remarks>
/// This method exists for backward compatibility. New code should use
/// <see cref="CreateSecureHttpClient(OtlpTlsOptions, Action{HttpClient}?)"/>.
/// </remarks>
public static HttpClient CreateMtlsHttpClient(
OtlpMtlsOptions mtlsOptions,
Action<HttpClient>? configureClient = null)
{
return CreateSecureHttpClient(mtlsOptions, configureClient);
}

/// <summary>
/// HttpClientHandler that applies TLS configuration based on loaded certificates.
/// </summary>
private sealed class TlsHttpClientHandler : HttpClientHandler
{
private readonly X509Certificate2? caCertificate;
private readonly X509Certificate2? clientCertificate;
private bool disposed;

internal MtlsHttpClientHandler(
X509Certificate2? clientCertificate,
X509Certificate2? caCertificate)
internal TlsHttpClientHandler(
X509Certificate2? caCertificate,
X509Certificate2? clientCertificate)
{
this.clientCertificate = clientCertificate;
this.caCertificate = caCertificate;
this.CheckCertificateRevocationList = true;
this.clientCertificate = clientCertificate;

this.ConfigureTls();
}

if (clientCertificate != null)
protected override void Dispose(bool disposing)
{
Comment thread
sandy2008 marked this conversation as resolved.
if (disposing && !this.disposed)
{
this.ClientCertificates.Add(clientCertificate);
this.ClientCertificateOptions = ClientCertificateOption.Manual;
this.clientCertificate?.Dispose();
this.caCertificate?.Dispose();
this.disposed = true;
}

if (caCertificate != null)
base.Dispose(disposing);
}

private void ConfigureTls()
{
this.CheckCertificateRevocationList = true;
Comment thread
sandy2008 marked this conversation as resolved.

this.ConfigureClientCertificate();
this.ConfigureCaCertificateValidation();
}

private void ConfigureClientCertificate()
{
if (this.clientCertificate == null)
{
this.ServerCertificateCustomValidationCallback = (
httpRequestMessage,
cert,
chain,
sslPolicyErrors) =>
{
if (cert == null || chain == null)
{
return false;
}

return OtlpCertificateManager.ValidateServerCertificate(
cert,
chain,
sslPolicyErrors,
caCertificate);
};
return;
}

this.ClientCertificates.Add(this.clientCertificate);
this.ClientCertificateOptions = ClientCertificateOption.Manual;
}

protected override void Dispose(bool disposing)
private void ConfigureCaCertificateValidation()
{
if (disposing)
if (this.caCertificate == null)
{
this.caCertificate?.Dispose();
this.clientCertificate?.Dispose();
return;
}

base.Dispose(disposing);
var caCert = this.caCertificate;
Comment thread
sandy2008 marked this conversation as resolved.
Outdated
this.ServerCertificateCustomValidationCallback = (
httpRequestMessage,
cert,
chain,
sslPolicyErrors) =>
{
if (cert == null || chain == null)
{
return false;
}

return OtlpCertificateManager.ValidateServerCertificate(
Comment thread
sandy2008 marked this conversation as resolved.
cert,
chain,
sslPolicyErrors,
caCert);
};
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ 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 OtlpSecureHttpClientFactory.CreateSecureHttpClient(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,48 @@

namespace OpenTelemetry.Exporter;

internal sealed class OtlpMtlsOptions
/// <summary>
/// Represents mTLS (mutual TLS) configuration options for OTLP exporter.
/// Extends <see cref="OtlpTlsOptions"/> with client certificate authentication.
/// </summary>
/// <remarks>
/// 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 <see cref="OtlpTlsOptions"/> directly.
/// </remarks>
internal sealed class OtlpMtlsOptions : OtlpTlsOptions
{
/// <summary>
/// Gets or sets the path to the CA certificate file in PEM format.
/// </summary>
public string? CaCertificatePath { get; set; }

/// <summary>
/// Gets or sets the path to the client certificate file in PEM format.
/// </summary>
/// <remarks>
/// Corresponds to the OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE environment variable.
/// This is used for client authentication in mTLS scenarios.
/// </remarks>
public string? ClientCertificatePath { get; set; }

/// <summary>
/// Gets or sets the path to the client private key file in PEM format.
/// </summary>
/// <remarks>
/// Corresponds to the OTEL_EXPORTER_OTLP_CLIENT_KEY environment variable.
/// Required when the client certificate file does not include the private key.
/// </remarks>
public string? ClientKeyPath { get; set; }

/// <summary>
/// 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.
/// </summary>
public bool EnableCertificateChainValidation { get; set; } = true;
/// <remarks>
/// Returns true when client certificate is configured for mutual authentication.
/// Note: Having only <see cref="OtlpTlsOptions.CaCertificatePath"/> does not constitute mTLS.
/// </remarks>
public override bool IsMtlsEnabled =>
!string.IsNullOrWhiteSpace(this.ClientCertificatePath);

/// <summary>
/// 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.
/// </summary>
public bool IsEnabled =>
!string.IsNullOrWhiteSpace(this.ClientCertificatePath)
Expand Down
Loading