From a363508fa61d66e615b6bb74382302b6c06fe60e Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Fri, 20 Jun 2025 15:29:05 +0900 Subject: [PATCH 01/37] feat(): add mtls support --- .../.publicApi/Stable/PublicAPI.Unshipped.txt | 1 + ...penTelemetryProtocolExporterEventSource.cs | 70 ++++ .../OtlpMtlsCertificateManager.cs | 381 ++++++++++++++++++ .../OtlpMtlsHttpClientFactory.cs | 189 +++++++++ .../OtlpExporterOptions.cs | 45 ++- .../OtlpMtlsOptions.cs | 66 +++ .../OtlpMtlsCertificateManagerTests.cs | 192 +++++++++ .../OtlpMtlsHttpClientFactoryTests.cs | 155 +++++++ .../OtlpMtlsOptionsTests.cs | 69 ++++ 9 files changed, 1158 insertions(+), 10 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..8b137891791 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index c4d21d92370..1cb4ce22af5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -240,4 +240,74 @@ void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, str { this.InvalidConfigurationValue(key, value); } + +#if NET8_0_OR_GREATER + [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 = "File permission check failed for {0} at '{1}'. Error: {2}", + Level = EventLevel.Warning)] + internal void MtlsFilePermissionCheckFailed( + string certificateType, + string filePath, + string error) => this.WriteEvent(29, certificateType, filePath, error); + + [Event( + 30, + Message = "File permission warning for {0} at '{1}': {2}", + Level = EventLevel.Warning)] + internal void MtlsFilePermissionWarning( + string certificateType, + string filePath, + string warning) => this.WriteEvent(30, certificateType, filePath, warning); + + [Event( + 31, + Message = "{0} chain validation failed for certificate '{1}'. Errors: {2}", + Level = EventLevel.Error)] + internal void MtlsCertificateChainValidationFailed( + string certificateType, + string subject, + string errors) => this.WriteEvent(31, certificateType, subject, errors); + + [Event( + 32, + Message = "{0} chain validated successfully for certificate '{1}'.", + Level = EventLevel.Informational)] + internal void MtlsCertificateChainValidated(string certificateType, string subject) => + this.WriteEvent(32, certificateType, subject); + + [Event( + 33, + Message = "Server certificate validated successfully for '{0}'.", + Level = EventLevel.Informational)] + internal void MtlsServerCertificateValidated(string subject) => this.WriteEvent(33, subject); + + [Event( + 34, + Message = "Server certificate validation failed for '{0}'. Errors: {1}", + Level = EventLevel.Error)] + internal void MtlsServerCertificateValidationFailed(string subject, string errors) => + this.WriteEvent(34, subject, errors); + + [Event( + 35, + Message = "mTLS configuration enabled. Client certificate: '{0}'.", + Level = EventLevel.Informational)] + internal void MtlsConfigurationEnabled(string clientCertificateSubject) => + this.WriteEvent(35, 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..0eb409edd11 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -0,0 +1,381 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +using System.Net.Security; +using System.Security.AccessControl; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; + +/// +/// Manages certificate loading, validation, and security checks for mTLS connections. +/// +internal static class OtlpMtlsCertificateManager +{ + /// + /// Loads a CA certificate from a PEM file. + /// + /// Path to the CA certificate file. + /// Whether to check file permissions. + /// The loaded CA certificate. + /// Thrown when the certificate file is not found. + /// Thrown when file permissions are inadequate. + /// Thrown when the certificate cannot be loaded. + public static X509Certificate2 LoadCaCertificate( + string caCertificatePath, + bool enableFilePermissionChecks = true) + { + ValidateFileExists(caCertificatePath, "CA certificate"); + + if (enableFilePermissionChecks) + { + ValidateFilePermissions(caCertificatePath, "CA certificate"); + } + + try + { + var caCertificate = X509Certificate2.CreateFromPemFile(caCertificatePath); + + OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( + "CA certificate", + caCertificatePath); + + return caCertificate; + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( + "CA certificate", + caCertificatePath, + ex.Message); + throw new InvalidOperationException( + $"Failed to load CA certificate from '{caCertificatePath}': {ex.Message}", + ex); + } + } + + /// + /// Loads a client certificate with its private key from PEM files. + /// + /// Path to the client certificate file. + /// Path to the client private key file. + /// Whether to check file permissions. + /// The loaded client certificate with private key. + /// Thrown when certificate or key files are not found. + /// Thrown when file permissions are inadequate. + /// Thrown when the certificate cannot be loaded. + public static X509Certificate2 LoadClientCertificate( + string clientCertificatePath, + string clientKeyPath, + bool enableFilePermissionChecks = true) + { + ValidateFileExists(clientCertificatePath, "Client certificate"); + ValidateFileExists(clientKeyPath, "Client private key"); + + if (enableFilePermissionChecks) + { + ValidateFilePermissions(clientCertificatePath, "Client certificate"); + ValidateFilePermissions(clientKeyPath, "Client private key"); + } + + try + { + var clientCertificate = X509Certificate2.CreateFromPemFile( + clientCertificatePath, + clientKeyPath); + + if (!clientCertificate.HasPrivateKey) + { + throw new InvalidOperationException( + "Client certificate does not have an associated private key."); + } + + OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( + "Client certificate", + clientCertificatePath); + + return clientCertificate; + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( + "Client certificate", + clientCertificatePath, + ex.Message); + throw new InvalidOperationException( + $"Failed to load client certificate from '{clientCertificatePath}' and key from '{clientKeyPath}': {ex.Message}", + ex); + } + } + + /// + /// Validates a certificate chain and checks for any errors. + /// + /// 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; + } + } + + /// + /// Creates a server certificate validation callback that uses a custom CA certificate. + /// + /// The CA certificate to use for validation. + /// A validation callback function. + public static Func< + X509Certificate2, + X509Chain, + SslPolicyErrors, + bool> CreateServerCertificateValidationCallback(X509Certificate2 caCertificate) + { + return (serverCert, chain, sslPolicyErrors) => + { + 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 ( + rootCert.Thumbprint.Equals( + 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}"); + } + } + + private static void ValidateFilePermissions(string filePath, string fileType) + { + try + { + if (OperatingSystem.IsWindows()) + { + ValidateWindowsFilePermissions(filePath, fileType); + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + ValidateUnixFilePermissions(filePath, fileType); + } + + // For other platforms, skip permission validation + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionCheckFailed( + fileType, + filePath, + ex.Message); + throw new UnauthorizedAccessException( + $"File permission check failed for {fileType} at '{filePath}': {ex.Message}", + ex); + } + } + + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private static void ValidateWindowsFilePermissions(string filePath, string fileType) + { + var fileInfo = new FileInfo(filePath); + var fileSecurity = fileInfo.GetAccessControl(); + var accessRules = fileSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); + + var currentUser = WindowsIdentity.GetCurrent(); + bool hasReadAccess = false; + bool hasRestrictedAccess = true; + + foreach (FileSystemAccessRule rule in accessRules) + { + var identity = rule.IdentityReference as SecurityIdentifier; + + // Check if current user has read access + if (identity != null && ( + currentUser.User?.Equals(identity) == true + || currentUser.Groups?.Contains(identity) == true)) + { + if ( + rule.AccessControlType == AccessControlType.Allow + && (rule.FileSystemRights & FileSystemRights.ReadData) != 0) + { + hasReadAccess = true; + } + } + + // Check for overly permissive access (e.g., Everyone, Users group with write access) + if ( + rule.AccessControlType == AccessControlType.Allow + && ( + rule.FileSystemRights + & (FileSystemRights.WriteData | FileSystemRights.FullControl)) != 0) + { + var wellKnownSids = new[] + { + WellKnownSidType.WorldSid, // Everyone + WellKnownSidType.AuthenticatedUserSid, // Authenticated Users + WellKnownSidType.BuiltinUsersSid, // Users + }; + + foreach (var sidType in wellKnownSids) + { + var wellKnownSid = new SecurityIdentifier(sidType, null); + if (identity?.Equals(wellKnownSid) == true) + { + hasRestrictedAccess = false; + break; + } + } + } + } + + if (!hasReadAccess) + { + throw new UnauthorizedAccessException( + $"Current user does not have read access to {fileType} file."); + } + + if (!hasRestrictedAccess) + { + OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionWarning( + fileType, + filePath, + "File has overly permissive access rights. Consider restricting access to improve security."); + } + } + + private static void ValidateUnixFilePermissions(string filePath, string fileType) + { + var fileInfo = new FileInfo(filePath); + + // On Unix systems, we can check if the file is readable by the current user + // by attempting to open it for reading + try + { + using var stream = fileInfo.OpenRead(); + } + catch (UnauthorizedAccessException) + { + throw new UnauthorizedAccessException( + $"Current user does not have read access to {fileType} file."); + } + + // For Unix systems, we recommend checking file permissions externally + // as .NET doesn't provide detailed Unix permission APIs + OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionWarning( + fileType, + filePath, + "Consider verifying that file permissions are set to 400 (read-only for owner) for enhanced security."); + } +} + +#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..47fb533ee98 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -0,0 +1,189 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +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. + /// The base HttpClient factory to use. + /// An HttpClient configured for mTLS. + public static HttpClient CreateMtlsHttpClient( + OtlpMtlsOptions mtlsOptions, + Func baseFactory) + { + ArgumentNullException.ThrowIfNull(mtlsOptions); + ArgumentNullException.ThrowIfNull(baseFactory); + + if (!mtlsOptions.IsEnabled) + { + return baseFactory(); + } + + HttpClientHandler? handler = null; + X509Certificate2? caCertificate = null; + X509Certificate2? clientCertificate = null; + + try + { + // Load certificates + if (!string.IsNullOrEmpty(mtlsOptions.CaCertificatePath)) + { + caCertificate = OtlpMtlsCertificateManager.LoadCaCertificate( + mtlsOptions.CaCertificatePath, + mtlsOptions.EnableFilePermissionChecks); + + if (mtlsOptions.EnableCertificateChainValidation) + { + OtlpMtlsCertificateManager.ValidateCertificateChain( + caCertificate, + "CA certificate"); + } + } + + if (!string.IsNullOrEmpty(mtlsOptions.ClientCertificatePath)) + { + if (string.IsNullOrEmpty(mtlsOptions.ClientKeyPath)) + { + // Check if certificate file exists to provide appropriate error message + if (!File.Exists(mtlsOptions.ClientCertificatePath)) + { + throw new FileNotFoundException($"Certificate file not found at path: {mtlsOptions.ClientCertificatePath}"); + } + } + else + { + clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate( + mtlsOptions.ClientCertificatePath, + mtlsOptions.ClientKeyPath, + mtlsOptions.EnableFilePermissionChecks); + + if (mtlsOptions.EnableCertificateChainValidation) + { + OtlpMtlsCertificateManager.ValidateCertificateChain( + clientCertificate, + "Client certificate"); + } + + 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; + } + + var serverCertValidationCallback = + OtlpMtlsCertificateManager.CreateServerCertificateValidationCallback( + caCertificate); + return serverCertValidationCallback(cert, chain, sslPolicyErrors); + }; + } + else if (mtlsOptions.ServerCertificateValidationCallback != null) + { + handler.ServerCertificateCustomValidationCallback = ( + httpRequestMessage, + cert, + chain, + sslPolicyErrors) => + { + if (cert == null || chain == null) + { + return false; + } + + return mtlsOptions.ServerCertificateValidationCallback( + cert, + chain, + sslPolicyErrors); + }; + } + + // Get base HttpClient to copy settings + var baseClient = baseFactory(); + var mtlsClient = new HttpClient(handler, disposeHandler: true); + + // Copy settings from base client + mtlsClient.Timeout = baseClient.Timeout; + mtlsClient.BaseAddress = baseClient.BaseAddress; + + // Copy default headers + foreach (var header in baseClient.DefaultRequestHeaders) + { + mtlsClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + + // Dispose the base client as we're not using it + baseClient.Dispose(); + + // Dispose certificates as they are no longer needed after being added to the handler + caCertificate?.Dispose(); + clientCertificate?.Dispose(); + + return mtlsClient; + } + catch (Exception ex) + { + // Dispose resources if something went wrong + handler?.Dispose(); + caCertificate?.Dispose(); + clientCertificate?.Dispose(); + + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); + throw; + } + } + + /// + /// Creates an HttpClient factory that supports mTLS configuration. + /// + /// The mTLS configuration options. + /// The base HttpClient factory to use. + /// A factory function that creates mTLS-configured HttpClient instances. + public static Func CreateMtlsHttpClientFactory( + OtlpMtlsOptions? mtlsOptions, + Func baseFactory) + { + if (mtlsOptions == null || !mtlsOptions.IsEnabled) + { + return baseFactory; + } + + return () => CreateMtlsHttpClient(mtlsOptions, baseFactory); + } +} + +#endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 91ebfdbd3e1..c091667e3dc 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,10 +72,28 @@ internal OtlpExporterOptions( this.DefaultHttpClientFactory = () => { - return new HttpClient + var baseClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), }; + +#if NET8_0_OR_GREATER + // If mTLS is configured, create an mTLS-enabled client + if (this.MtlsOptions?.IsEnabled == true) + { + var mtlsClient = OtlpMtlsHttpClientFactory.CreateMtlsHttpClient( + this.MtlsOptions, + () => + new HttpClient + { + Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), + }); + baseClient.Dispose(); + return mtlsClient; + } +#endif + + return baseClient; }; this.BatchExportProcessorOptions = defaultBatchOptions!; @@ -158,6 +176,14 @@ public Func HttpClientFactory /// internal bool AppendSignalPathToEndpoint { get; private set; } = true; +#if NET8_0_OR_GREATER + /// + /// Gets or sets the mTLS (mutual TLS) configuration options. + /// This property is only available on .NET 8.0 and later versions. + /// + internal OtlpMtlsOptions? MtlsOptions { get; set; } +#endif + internal bool HasData => this.protocol.HasValue || this.endpoint != null @@ -167,8 +193,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 +213,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; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs new file mode 100644 index 00000000000..1e565941d70 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace OpenTelemetry.Exporter; + +/// +/// Configuration options for mTLS (mutual TLS) authentication in OTLP exporters. +/// +/// +/// This class is only available on .NET 8.0 and later versions. +/// +internal 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 file permission checks. + /// When enabled, the exporter will verify that certificate files have appropriate permissions. + /// + public bool EnableFilePermissionChecks { get; set; } = true; + + /// + /// 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 or sets the server certificate validation callback. + /// This callback is used to validate the server certificate during TLS handshake. + /// If not set, the default certificate validation logic will be used. + /// + public Func< + X509Certificate2, + X509Chain, + SslPolicyErrors, + bool + >? ServerCertificateValidationCallback + { get; set; } + + /// + /// 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/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs new file mode 100644 index 00000000000..f8741740f68 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs @@ -0,0 +1,192 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +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 +4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV +WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO +PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG +HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 +ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123 +4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV +WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO +PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG +HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 +ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123 +4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCD +-----END CERTIFICATE-----"; + + [Xunit.Fact] + public void LoadClientCertificate_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist() + { + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate( + "/nonexistent/client.crt", + "/nonexistent/client.key")); + + Xunit.Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Xunit.Assert.Contains("/nonexistent/client.crt", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Xunit.Fact] + public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFileDoesNotExist() + { + var tempCertFile = Path.GetTempFileName(); + File.WriteAllText(tempCertFile, TestCertPem); + + try + { + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate( + tempCertFile, + "/nonexistent/client.key")); + + Xunit.Assert.Contains("Private key file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Xunit.Assert.Contains("/nonexistent/client.key", exception.Message, StringComparison.OrdinalIgnoreCase); + } + finally + { + File.Delete(tempCertFile); + } + } + + [Xunit.Fact] + public void LoadCaCertificate_ThrowsFileNotFoundException_WhenTrustStoreFileDoesNotExist() + { + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate("/nonexistent/ca.crt")); + + Xunit.Assert.Contains("CA certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Xunit.Assert.Contains("/nonexistent/ca.crt", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Xunit.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 = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(tempCertFile, tempKeyFile)); + + Xunit.Assert.Contains( + "Failed to load client certificate", + exception.Message, + StringComparison.OrdinalIgnoreCase); + } + finally + { + File.Delete(tempCertFile); + File.Delete(tempKeyFile); + } + } + + [Xunit.Fact] + public void LoadCaCertificate_ThrowsInvalidOperationException_WhenTrustStoreFileIsEmpty() + { + var tempTrustStoreFile = Path.GetTempFileName(); + File.WriteAllText(tempTrustStoreFile, string.Empty); + + try + { + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate(tempTrustStoreFile)); + + Xunit.Assert.Contains( + "Failed to load CA certificate", + exception.Message, + StringComparison.OrdinalIgnoreCase); + } + finally + { + File.Delete(tempTrustStoreFile); + } + } + + [Xunit.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 + Xunit.Assert.True(result || !result); // Just check that it returns a boolean + } + + [Xunit.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 + Xunit.Assert.True(result || !result); + } + + 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..4effd37827f --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs @@ -0,0 +1,155 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpMtlsHttpClientFactoryTests +{ + [Xunit.Fact] + public void CreateHttpClient_ReturnsHttpClient_WhenMtlsIsDisabled() + { + var baseFactory = () => new HttpClient(); + var options = new OtlpMtlsOptions(); // Disabled by default + + using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory); + + Xunit.Assert.NotNull(httpClient); + Xunit.Assert.IsType(httpClient); + } + + [Xunit.Fact] + public void CreateHttpClient_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist() + { + var baseFactory = () => new HttpClient(); + var options = new OtlpMtlsOptions { ClientCertificatePath = "/nonexistent/client.crt" }; + + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory)); + + Xunit.Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Xunit.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, "testpassword"); + File.WriteAllBytes(tempCertFile, certBytes); + + var baseFactory = () => new HttpClient(); + var options = new OtlpMtlsOptions + { + ClientCertificatePath = tempCertFile, + + // Note: Password support would need to be added to OtlpMtlsOptions + EnableCertificateChainValidation = false, // Ignore validation for test cert + }; + + using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory); + + Xunit.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) + { + Xunit.Assert.NotEmpty(handler.ClientCertificates); + } + } + finally + { + if (File.Exists(tempCertFile)) + { + File.Delete(tempCertFile); + } + } + } + + [Xunit.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 baseFactory = () => new HttpClient(); + var options = new OtlpMtlsOptions + { + CaCertificatePath = tempTrustStoreFile, + }; + + using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory); + + Xunit.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) + { + Xunit.Assert.NotNull(handler.ServerCertificateCustomValidationCallback); + } + } + finally + { + if (File.Exists(tempTrustStoreFile)) + { + File.Delete(tempTrustStoreFile); + } + } + } + + [Xunit.Fact] + public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenBaseFactoryIsNull() + { + var options = new OtlpMtlsOptions(); + + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, null!)); + + Xunit.Assert.Equal("baseFactory", exception.ParamName); + } + + [Xunit.Fact] + public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenOptionsIsNull() + { + var baseFactory = () => new HttpClient(); + + var exception = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(null!, baseFactory)); + + Xunit.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..f4210ad8be0 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs @@ -0,0 +1,69 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +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.EnableFilePermissionChecks); + 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", + EnableFilePermissionChecks = false, + 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.EnableFilePermissionChecks); + 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 From faba6ab22a5c58f9d5b705d62b1d2156616b12f2 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Fri, 20 Jun 2025 17:46:43 +0900 Subject: [PATCH 02/37] refactor(mtls): extract repeated string literals to constants in OtlpMtlsCertificateManager --- .../OtlpMtlsCertificateManager.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 0eb409edd11..7f2c85459fd 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -15,6 +15,10 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; /// internal static class OtlpMtlsCertificateManager { + private const string CaCertificateType = "CA certificate"; + private const string ClientCertificateType = "Client certificate"; + private const string ClientPrivateKeyType = "Client private key"; + /// /// Loads a CA certificate from a PEM file. /// @@ -28,11 +32,11 @@ public static X509Certificate2 LoadCaCertificate( string caCertificatePath, bool enableFilePermissionChecks = true) { - ValidateFileExists(caCertificatePath, "CA certificate"); + ValidateFileExists(caCertificatePath, CaCertificateType); if (enableFilePermissionChecks) { - ValidateFilePermissions(caCertificatePath, "CA certificate"); + ValidateFilePermissions(caCertificatePath, CaCertificateType); } try @@ -40,7 +44,7 @@ public static X509Certificate2 LoadCaCertificate( var caCertificate = X509Certificate2.CreateFromPemFile(caCertificatePath); OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( - "CA certificate", + CaCertificateType, caCertificatePath); return caCertificate; @@ -48,7 +52,7 @@ public static X509Certificate2 LoadCaCertificate( catch (Exception ex) { OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( - "CA certificate", + CaCertificateType, caCertificatePath, ex.Message); throw new InvalidOperationException( @@ -72,13 +76,13 @@ public static X509Certificate2 LoadClientCertificate( string clientKeyPath, bool enableFilePermissionChecks = true) { - ValidateFileExists(clientCertificatePath, "Client certificate"); - ValidateFileExists(clientKeyPath, "Client private key"); + ValidateFileExists(clientCertificatePath, ClientCertificateType); + ValidateFileExists(clientKeyPath, ClientPrivateKeyType); if (enableFilePermissionChecks) { - ValidateFilePermissions(clientCertificatePath, "Client certificate"); - ValidateFilePermissions(clientKeyPath, "Client private key"); + ValidateFilePermissions(clientCertificatePath, ClientCertificateType); + ValidateFilePermissions(clientKeyPath, ClientPrivateKeyType); } try @@ -94,7 +98,7 @@ public static X509Certificate2 LoadClientCertificate( } OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoaded( - "Client certificate", + ClientCertificateType, clientCertificatePath); return clientCertificate; @@ -102,7 +106,7 @@ public static X509Certificate2 LoadClientCertificate( catch (Exception ex) { OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateLoadFailed( - "Client certificate", + ClientCertificateType, clientCertificatePath, ex.Message); throw new InvalidOperationException( From 3044fc32c84820a616c72e377bc984f89090c815 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Fri, 20 Jun 2025 18:46:09 +0900 Subject: [PATCH 03/37] feat(): add mtls support --- .../OtlpSpecConfigDefinitions.cs | 5 + .../OtlpExporterOptions.cs | 33 ++++++ .../README.md | 9 ++ .../OtlpExporterOptionsTests.cs | 103 ++++++++++++++++++ 4 files changed, 150 insertions(+) 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 c091667e3dc..b9271a53b2e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -311,5 +311,38 @@ private void ApplyConfiguration( { throw new NotSupportedException($"OtlpExporterOptionsConfigurationType '{configurationType}' is not supported."); } + +#if NET8_0_OR_GREATER + // Apply mTLS configuration from environment variables + this.ApplyMtlsConfiguration(configuration); +#endif + } + +#if NET8_0_OR_GREATER + 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 OtlpMtlsOptions(); + this.MtlsOptions.CaCertificatePath = caCertPath; + } + + // Check and apply client certificate path from environment variable + if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientCertificateEnvVarName, out var clientCertPath)) + { + this.MtlsOptions ??= new OtlpMtlsOptions(); + this.MtlsOptions.ClientCertificatePath = clientCertPath; + } + + // Check and apply client key path from environment variable + if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientKeyEnvVarName, out var clientKeyPath)) + { + this.MtlsOptions ??= new OtlpMtlsOptions(); + this.MtlsOptions.ClientKeyPath = clientKeyPath; + } } +#endif } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index e12ba68ff22..9ba0f949656 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..03fa6a166ce 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 NET8_0_OR_GREATER + [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 } From 7368c9e7f4c8c1c1545fdce97325c5512c3ba3f0 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Fri, 20 Jun 2025 19:16:50 +0900 Subject: [PATCH 04/37] feat(): add mtls configurations for certs --- .../OtlpMtlsCertificateManager.cs | 82 ++++++++- .../OtlpSpecConfigDefinitions.cs | 4 + .../README.md | 12 +- .../OtlpMtlsCertificateManagerTests.cs | 161 ++++++++++++++++++ .../OtlpSpecConfigDefinitionsTests.cs | 54 ++++++ 5 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 7f2c85459fd..03540d7c34d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -7,6 +7,7 @@ using System.Security.AccessControl; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; +using Microsoft.Extensions.Configuration; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -116,7 +117,7 @@ public static X509Certificate2 LoadClientCertificate( } /// - /// Validates a certificate chain and checks for any errors. + /// Validates the certificate chain for a given certificate. /// /// The certificate to validate. /// Type description for logging (e.g., "Client certificate"). @@ -124,6 +125,21 @@ public static X509Certificate2 LoadClientCertificate( public static bool ValidateCertificateChain( X509Certificate2 certificate, string certificateType) + { + return ValidateCertificateChain(certificate, certificateType, null); + } + + /// + /// Validates the certificate chain for a given certificate with optional configuration. + /// + /// The certificate to validate. + /// Type description for logging (e.g., "Client certificate"). + /// Optional configuration to read environment variables from. + /// True if the certificate chain is valid; otherwise, false. + public static bool ValidateCertificateChain( + X509Certificate2 certificate, + string certificateType, + IConfiguration? configuration) { try { @@ -131,8 +147,14 @@ public static bool ValidateCertificateChain( // Configure chain policy chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; - chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; - chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + + // Configure RevocationMode from environment variable or use default + var revocationMode = GetRevocationModeFromConfiguration(configuration); + chain.ChainPolicy.RevocationMode = revocationMode; + + // Configure RevocationFlag from environment variable or use default + var revocationFlag = GetRevocationFlagFromConfiguration(configuration); + chain.ChainPolicy.RevocationFlag = revocationFlag; bool isValid = chain.Build(certificate); @@ -380,6 +402,60 @@ private static void ValidateUnixFilePermissions(string filePath, string fileType filePath, "Consider verifying that file permissions are set to 400 (read-only for owner) for enhanced security."); } + + /// + /// Gets the X509RevocationMode from configuration or returns the default value. + /// + /// Configuration to read from. + /// The configured revocation mode or default (Online). + private static X509RevocationMode GetRevocationModeFromConfiguration(IConfiguration? configuration) + { + if (configuration == null) + { + return X509RevocationMode.Online; + } + + if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, out var modeString)) + { + if (Enum.TryParse(modeString, true, out var mode)) + { + return mode; + } + + ((IConfigurationExtensionsLogger)OpenTelemetryProtocolExporterEventSource.Log).LogInvalidConfigurationValue( + OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, + modeString); + } + + return X509RevocationMode.Online; + } + + /// + /// Gets the X509RevocationFlag from configuration or returns the default value. + /// + /// Configuration to read from. + /// The configured revocation flag or default (ExcludeRoot). + private static X509RevocationFlag GetRevocationFlagFromConfiguration(IConfiguration? configuration) + { + if (configuration == null) + { + return X509RevocationFlag.ExcludeRoot; + } + + if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, out var flagString)) + { + if (Enum.TryParse(flagString, true, out var flag)) + { + return flag; + } + + ((IConfigurationExtensionsLogger)OpenTelemetryProtocolExporterEventSource.Log).LogInvalidConfigurationValue( + OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, + flagString); + } + + return X509RevocationFlag.ExcludeRoot; + } } #endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs index 8e8fd497b0d..a98849c01dc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs @@ -36,4 +36,8 @@ internal static class OtlpSpecConfigDefinitions 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"; + + // Certificate validation environment variables + public const string CertificateRevocationModeEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE"; + public const string CertificateRevocationFlagEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG"; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 9ba0f949656..50debea0014 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -453,11 +453,13 @@ or reader 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)| + | 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)| + | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` | + | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` | * Logs: diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs index f8741740f68..1d74346848a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs @@ -3,6 +3,8 @@ #if NET8_0_OR_GREATER +using Microsoft.Extensions.Configuration; + namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpMtlsCertificateManagerTests @@ -157,6 +159,165 @@ public void ValidateCertificateChain_ReturnsResult_WithValidCertificate() Xunit.Assert.True(result || !result); } + [Xunit.Fact] + public void ValidateCertificateChain_UsesDefaultConfiguration_WhenConfigurationIsNull() + { + using var cert = CreateSelfSignedCertificate(); + + // Both overloads should work + var result1 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate"); + var result2 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", null); + + // Results should be the same since both use defaults + Xunit.Assert.Equal(result1, result2); + } + + [Xunit.Fact] + public void ValidateCertificateChain_UsesRevocationModeFromConfiguration() + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "NoCheck"), + }) + .Build(); + + // Should not throw when using NoCheck mode + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing + Xunit.Assert.True(result || !result); + } + + [Xunit.Fact] + public void ValidateCertificateChain_UsesRevocationFlagFromConfiguration() + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "EntireChain"), + }) + .Build(); + + // Should not throw when using EntireChain flag + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing + Xunit.Assert.True(result || !result); + } + + [Xunit.Fact] + public void ValidateCertificateChain_UsesBothRevocationConfigurationValues() + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "Offline"), + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "EndCertificateOnly"), + }) + .Build(); + + // Should not throw when using both configuration values + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing + Xunit.Assert.True(result || !result); + } + + [Xunit.Fact] + public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationMode() + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "InvalidMode"), + }) + .Build(); + + // Should not throw even with invalid configuration value (should use default) + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing and use default Online mode + Xunit.Assert.True(result || !result); + } + + [Xunit.Fact] + public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationFlag() + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "InvalidFlag"), + }) + .Build(); + + // Should not throw even with invalid configuration value (should use default) + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing and use default ExcludeRoot flag + Xunit.Assert.True(result || !result); + } + + [Xunit.Theory] + [Xunit.InlineData("Online")] + [Xunit.InlineData("Offline")] + [Xunit.InlineData("NoCheck")] + [Xunit.InlineData("online")] + [Xunit.InlineData("OFFLINE")] + [Xunit.InlineData("nocheck")] + public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationMode(string revocationMode) + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, revocationMode), + }) + .Build(); + + // Should handle case-insensitive enum parsing + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing + Xunit.Assert.True(result || !result); + } + + [Xunit.Theory] + [Xunit.InlineData("ExcludeRoot")] + [Xunit.InlineData("EntireChain")] + [Xunit.InlineData("EndCertificateOnly")] + [Xunit.InlineData("excluderoot")] + [Xunit.InlineData("ENTIRECHAIN")] + [Xunit.InlineData("endcertificateonly")] + public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationFlag(string revocationFlag) + { + using var cert = CreateSelfSignedCertificate(); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, revocationFlag), + }) + .Build(); + + // Should handle case-insensitive enum parsing + var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); + + // The method should execute without throwing + Xunit.Assert.True(result || !result); + } + private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSignedCertificate() { using var rsa = System.Security.Cryptography.RSA.Create(2048); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs new file mode 100644 index 00000000000..ddb7add6919 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET8_0_OR_GREATER + +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +public class OtlpSpecConfigDefinitionsTests +{ + [Fact] + public void CertificateRevocationModeEnvVarName_HasCorrectValue() + { + Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE", OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName); + } + + [Fact] + public void CertificateRevocationFlagEnvVarName_HasCorrectValue() + { + Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName); + } + + [Fact] + public void AllEnvironmentVariableNames_AreUnique() + { + var envVars = new[] + { + OtlpSpecConfigDefinitions.DefaultEndpointEnvVarName, + OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName, + OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName, + OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName, + OtlpSpecConfigDefinitions.CertificateEnvVarName, + OtlpSpecConfigDefinitions.ClientKeyEnvVarName, + OtlpSpecConfigDefinitions.ClientCertificateEnvVarName, + OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, + OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, + }; + + var uniqueVars = envVars.Distinct().ToArray(); + + Assert.Equal(envVars.Length, uniqueVars.Length); + } + + [Fact] + public void CertificateRevocationEnvironmentVariables_FollowNamingConvention() + { + // All certificate-related environment variables should follow the OTEL_EXPORTER_OTLP_CERTIFICATE prefix + Assert.StartsWith("OTEL_EXPORTER_OTLP_CERTIFICATE", OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, StringComparison.Ordinal); + Assert.StartsWith("OTEL_EXPORTER_OTLP_CERTIFICATE", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, StringComparison.Ordinal); + } +} + +#endif From c488394ddf62c341de0305b5c390a4248b51fd93 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Mon, 23 Jun 2025 11:40:20 +0900 Subject: [PATCH 05/37] fix(mtls): include filePath in FileNotFoundException for better diagnostics Co-authored-by: Martin Costello --- .../Implementation/OtlpMtlsCertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 03540d7c34d..5548fa652b8 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -279,7 +279,7 @@ private static void ValidateFileExists(string filePath, string fileType) OpenTelemetryProtocolExporterEventSource.Log.MtlsCertificateFileNotFound( fileType, filePath); - throw new FileNotFoundException($"{fileType} file not found at path: {filePath}"); + throw new FileNotFoundException($"{fileType} file not found at path: {filePath}", filePath); } } From 472205afb04c1a857f10919c2cded77bc62f83ac Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Mon, 23 Jun 2025 11:41:18 +0900 Subject: [PATCH 06/37] fix(mtls): use string.Equals to safely compare thumbprints and avoid null reference Co-authored-by: Martin Costello --- .../Implementation/OtlpMtlsCertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 5548fa652b8..ed88ba45ee6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -237,7 +237,7 @@ public static Func< // Verify that the chain terminates with our CA var rootCert = chain.ChainElements[^1].Certificate; if ( - rootCert.Thumbprint.Equals( + string.Equals(rootCert.Thumbprint, caCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase)) { From 2d69b7c79a1c98f32ea81f8582d53ce7002e63b5 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Mon, 23 Jun 2025 11:43:28 +0900 Subject: [PATCH 07/37] fix(): remove redundant comment --- .../OtlpExporterOptions.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index b9271a53b2e..7a671163d41 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -177,10 +177,6 @@ public Func HttpClientFactory internal bool AppendSignalPathToEndpoint { get; private set; } = true; #if NET8_0_OR_GREATER - /// - /// Gets or sets the mTLS (mutual TLS) configuration options. - /// This property is only available on .NET 8.0 and later versions. - /// internal OtlpMtlsOptions? MtlsOptions { get; set; } #endif From 960037edf6eaa7251247ec384cab72ebfa1d5b2a Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Mon, 23 Jun 2025 11:44:13 +0900 Subject: [PATCH 08/37] fix(): remove redundant comment --- .../OtlpMtlsOptions.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 1e565941d70..2d1cb4d331b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -8,12 +8,6 @@ namespace OpenTelemetry.Exporter; -/// -/// Configuration options for mTLS (mutual TLS) authentication in OTLP exporters. -/// -/// -/// This class is only available on .NET 8.0 and later versions. -/// internal class OtlpMtlsOptions { /// From 6226ac5f546c0eb56afe203f54c6b0171f6688d4 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Mon, 23 Jun 2025 12:22:52 +0900 Subject: [PATCH 09/37] fix(): remove redundant comment --- .../Implementation/OtlpMtlsCertificateManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index ed88ba45ee6..a02267bd4d8 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -237,7 +237,8 @@ public static Func< // Verify that the chain terminates with our CA var rootCert = chain.ChainElements[^1].Certificate; if ( - string.Equals(rootCert.Thumbprint, + string.Equals( + rootCert.Thumbprint, caCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase)) { From 966d0766c9e76295e4690aa5a2c55c18115fc68b Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 01:51:17 +0900 Subject: [PATCH 10/37] feat(): add support for client key w/ password --- .../OtlpMtlsCertificateManager.cs | 39 +++++++++++++++++-- .../OtlpMtlsHttpClientFactory.cs | 1 + .../OtlpSpecConfigDefinitions.cs | 1 + .../OtlpExporterOptions.cs | 7 ++++ .../OtlpMtlsOptions.cs | 7 ++++ .../README.md | 1 + .../OtlpExporterOptionsTests.cs | 26 +++++++++++++ .../OtlpMtlsCertificateManagerTests.cs | 39 ++++++++++++++----- .../OtlpMtlsOptionsTests.cs | 2 + .../OtlpSpecConfigDefinitionsTests.cs | 7 ++++ 10 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index a02267bd4d8..8aed4313abb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -76,6 +76,26 @@ public static X509Certificate2 LoadClientCertificate( string clientCertificatePath, string clientKeyPath, bool enableFilePermissionChecks = true) + { + return LoadClientCertificate(clientCertificatePath, clientKeyPath, null, enableFilePermissionChecks); + } + + /// + /// Loads a client certificate with its private key from PEM files. + /// + /// Path to the client certificate file. + /// Path to the client private key file. + /// Password for the client private key file if it is encrypted. Can be null for unencrypted keys. + /// Whether to check file permissions. + /// The loaded client certificate with private key. + /// Thrown when certificate or key files are not found. + /// Thrown when file permissions are inadequate. + /// Thrown when the certificate cannot be loaded. + public static X509Certificate2 LoadClientCertificate( + string clientCertificatePath, + string clientKeyPath, + string? clientKeyPassword, + bool enableFilePermissionChecks = true) { ValidateFileExists(clientCertificatePath, ClientCertificateType); ValidateFileExists(clientKeyPath, ClientPrivateKeyType); @@ -88,9 +108,22 @@ public static X509Certificate2 LoadClientCertificate( try { - var clientCertificate = X509Certificate2.CreateFromPemFile( - clientCertificatePath, - clientKeyPath); + X509Certificate2 clientCertificate; + + // Choose the appropriate method based on whether a password is provided + if (!string.IsNullOrEmpty(clientKeyPassword)) + { + clientCertificate = X509Certificate2.CreateFromEncryptedPemFile( + clientCertificatePath, + clientKeyPath, + clientKeyPassword); + } + else + { + clientCertificate = X509Certificate2.CreateFromPemFile( + clientCertificatePath, + clientKeyPath); + } if (!clientCertificate.HasPrivateKey) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 47fb533ee98..5fc4d0a621d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -66,6 +66,7 @@ public static HttpClient CreateMtlsHttpClient( clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate( mtlsOptions.ClientCertificatePath, mtlsOptions.ClientKeyPath, + mtlsOptions.ClientKeyPassword, mtlsOptions.EnableFilePermissionChecks); if (mtlsOptions.EnableCertificateChainValidation) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs index a98849c01dc..02957c61825 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs @@ -36,6 +36,7 @@ internal static class OtlpSpecConfigDefinitions 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"; + public const string ClientKeyPasswordEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD"; // Certificate validation environment variables public const string CertificateRevocationModeEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE"; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 7a671163d41..e70e43ef7d7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -339,6 +339,13 @@ private void ApplyMtlsConfiguration(IConfiguration configuration) this.MtlsOptions ??= new OtlpMtlsOptions(); this.MtlsOptions.ClientKeyPath = clientKeyPath; } + + // Check and apply client key password from environment variable + if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName, out var clientKeyPassword)) + { + this.MtlsOptions ??= new OtlpMtlsOptions(); + this.MtlsOptions.ClientKeyPassword = clientKeyPassword; + } } #endif } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 2d1cb4d331b..bc8bca8b0fe 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -25,6 +25,13 @@ internal class OtlpMtlsOptions /// public string? ClientKeyPath { get; set; } + /// + /// Gets or sets the password for the client private key file when it is encrypted. + /// This is only used when the private key file is password-protected. + /// If not provided and the key file is encrypted, certificate loading will fail. + /// + public string? ClientKeyPassword { get; set; } + /// /// Gets or sets a value indicating whether to enable file permission checks. /// When enabled, the exporter will verify that certificate files have appropriate permissions. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 50debea0014..dbe284b52b6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -458,6 +458,7 @@ or reader | `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)| + | `OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD` | `ClientKeyPassword` | Password for encrypted client private key file (PEM)| | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` | | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` | diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 03fa6a166ce..26668244c29 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -342,6 +342,32 @@ public void OtlpExporterOptions_MtlsEnvironmentVariables_NoEnvironmentVariables( Assert.Null(options.MtlsOptions); } + [Fact] + public void OtlpExporterOptions_MtlsEnvironmentVariables_ClientKeyPassword() + { + // Test client key password environment variable + Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client.crt"); + Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/client.key"); + Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD", "secret123"); + + 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.Equal("secret123", options.MtlsOptions.ClientKeyPassword); + Assert.True(options.MtlsOptions.IsEnabled); + } + finally + { + Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", null); + Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", null); + Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD", null); + } + } + [Fact] public void OtlpExporterOptions_MtlsEnvironmentVariables_UsingIConfiguration() { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs index 1d74346848a..c4532ed5aae 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs @@ -29,16 +29,6 @@ public class OtlpMtlsCertificateManagerTests PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123 -4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV -WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO -PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG -HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 -ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123 -4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUV -WXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNO -PQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFG -HIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 -ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123 4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCD -----END CERTIFICATE-----"; @@ -318,6 +308,35 @@ public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationFlag(string Xunit.Assert.True(result || !result); } + [Xunit.Fact] + public void LoadClientCertificate_AcceptsPasswordParameter() + { + 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 accepts the password parameter + // 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 = Xunit.Assert.Throws(() => + OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate( + tempCertFile, + tempKeyFile, + "test-password")); + + // The exception should be about certificate loading, not method signature + Xunit.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); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs index f4210ad8be0..f7ab9654be3 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs @@ -29,6 +29,7 @@ public void Properties_CanBeSet() { ClientCertificatePath = "/path/to/client.crt", ClientKeyPath = "/path/to/client.key", + ClientKeyPassword = "secret123", CaCertificatePath = "/path/to/ca.crt", EnableFilePermissionChecks = false, EnableCertificateChainValidation = false, @@ -36,6 +37,7 @@ public void Properties_CanBeSet() Assert.Equal("/path/to/client.crt", options.ClientCertificatePath); Assert.Equal("/path/to/client.key", options.ClientKeyPath); + Assert.Equal("secret123", options.ClientKeyPassword); Assert.Equal("/path/to/ca.crt", options.CaCertificatePath); Assert.False(options.EnableFilePermissionChecks); Assert.False(options.EnableCertificateChainValidation); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs index ddb7add6919..fa5a5844318 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs @@ -21,6 +21,12 @@ public void CertificateRevocationFlagEnvVarName_HasCorrectValue() Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName); } + [Fact] + public void ClientKeyPasswordEnvVarName_HasCorrectValue() + { + Assert.Equal("OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD", OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName); + } + [Fact] public void AllEnvironmentVariableNames_AreUnique() { @@ -33,6 +39,7 @@ public void AllEnvironmentVariableNames_AreUnique() OtlpSpecConfigDefinitions.CertificateEnvVarName, OtlpSpecConfigDefinitions.ClientKeyEnvVarName, OtlpSpecConfigDefinitions.ClientCertificateEnvVarName, + OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName, OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, }; From 879cd6c2ecd9281401fa13c327c5eadd3aa55e6c Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 01:56:38 +0900 Subject: [PATCH 11/37] refactor(mtls): extract HttpClient default configuration to separate method --- .../OtlpMtlsHttpClientFactory.cs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 5fc4d0a621d..029c2573a6b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -133,28 +133,15 @@ public static HttpClient CreateMtlsHttpClient( }; } - // Get base HttpClient to copy settings - var baseClient = baseFactory(); - var mtlsClient = new HttpClient(handler, disposeHandler: true); + var client = new HttpClient(handler, disposeHandler: true); - // Copy settings from base client - mtlsClient.Timeout = baseClient.Timeout; - mtlsClient.BaseAddress = baseClient.BaseAddress; - - // Copy default headers - foreach (var header in baseClient.DefaultRequestHeaders) - { - mtlsClient.DefaultRequestHeaders.Add(header.Key, header.Value); - } - - // Dispose the base client as we're not using it - baseClient.Dispose(); + ConfigureHttpClientDefaults(client, baseFactory); // Dispose certificates as they are no longer needed after being added to the handler caCertificate?.Dispose(); clientCertificate?.Dispose(); - return mtlsClient; + return client; } catch (Exception ex) { @@ -185,6 +172,27 @@ public static Func CreateMtlsHttpClientFactory( return () => CreateMtlsHttpClient(mtlsOptions, baseFactory); } + + /// + /// Configures the HttpClient with default settings from the base factory. + /// + /// The HttpClient to configure. + /// The base HttpClient factory to get settings from. + private static void ConfigureHttpClientDefaults(HttpClient client, Func baseFactory) + { + // Get base HttpClient to copy settings + using var baseClient = baseFactory(); + + // Copy settings from base client + client.Timeout = baseClient.Timeout; + client.BaseAddress = baseClient.BaseAddress; + + // Copy default headers + foreach (var header in baseClient.DefaultRequestHeaders) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } } #endif From c1405cbc960f45c7e6070f12be83aca17850d212 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 02:00:23 +0900 Subject: [PATCH 12/37] refactor(mtls): eliminate magic strings in OtlpMtlsHttpClientFactory --- .../Implementation/OtlpMtlsCertificateManager.cs | 6 +++--- .../Implementation/OtlpMtlsHttpClientFactory.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 8aed4313abb..f84f36896c6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -16,9 +16,9 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; /// internal static class OtlpMtlsCertificateManager { - private const string CaCertificateType = "CA certificate"; - private const string ClientCertificateType = "Client certificate"; - private const string ClientPrivateKeyType = "Client private key"; + 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. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 029c2573a6b..b52531e02aa 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -47,7 +47,7 @@ public static HttpClient CreateMtlsHttpClient( { OtlpMtlsCertificateManager.ValidateCertificateChain( caCertificate, - "CA certificate"); + OtlpMtlsCertificateManager.CaCertificateType); } } @@ -73,7 +73,7 @@ public static HttpClient CreateMtlsHttpClient( { OtlpMtlsCertificateManager.ValidateCertificateChain( clientCertificate, - "Client certificate"); + OtlpMtlsCertificateManager.ClientCertificateType); } OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled( From 02b92406484cf6ca544fc80e738cd21dcdfefc9a Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 20:35:52 +0900 Subject: [PATCH 13/37] feat(mtls): adjust error msg Co-authored-by: Martin Costello --- .../Implementation/OtlpMtlsCertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index f84f36896c6..28ca498c420 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -426,7 +426,7 @@ private static void ValidateUnixFilePermissions(string filePath, string fileType catch (UnauthorizedAccessException) { throw new UnauthorizedAccessException( - $"Current user does not have read access to {fileType} file."); + $"Current user does not have read access to {fileType} file.", ex); } // For Unix systems, we recommend checking file permissions externally From 6ae5efe0876ebe77fb2082bfa32b43b328384f99 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 20:36:17 +0900 Subject: [PATCH 14/37] feat(mtls): adjust error msg Co-authored-by: Martin Costello --- .../Implementation/OtlpMtlsHttpClientFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index b52531e02aa..873d17fe921 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -58,7 +58,7 @@ public static HttpClient CreateMtlsHttpClient( // Check if certificate file exists to provide appropriate error message if (!File.Exists(mtlsOptions.ClientCertificatePath)) { - throw new FileNotFoundException($"Certificate file not found at path: {mtlsOptions.ClientCertificatePath}"); + throw new FileNotFoundException($"Certificate file not found at path: {mtlsOptions.ClientCertificatePath}", mtlsOptions.ClientCertificatePath); } } else From eadf9c23977aa83db64315850e12f5c0c72020e1 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 20:39:52 +0900 Subject: [PATCH 15/37] feat(mtls): adjust error msg --- .../Implementation/OtlpMtlsCertificateManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 28ca498c420..628471f072f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -423,10 +423,10 @@ private static void ValidateUnixFilePermissions(string filePath, string fileType { using var stream = fileInfo.OpenRead(); } - catch (UnauthorizedAccessException) + catch (UnauthorizedAccessException exception) { throw new UnauthorizedAccessException( - $"Current user does not have read access to {fileType} file.", ex); + $"Current user does not have read access to {fileType} file.", exception); } // For Unix systems, we recommend checking file permissions externally From 2c759c6bf9f97c706275f27be213e01825d5a704 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 20:44:41 +0900 Subject: [PATCH 16/37] chore(mtls): when mTLS file permission validation is skipped on unsupported platforms --- ...penTelemetryProtocolExporterEventSource.cs | 27 ++++++++++++------- .../OtlpMtlsCertificateManager.cs | 10 +++++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 1cb4ce22af5..ae1fd3b5382 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -276,38 +276,47 @@ internal void MtlsFilePermissionWarning( [Event( 31, + Message = "File permission validation skipped for {0} at '{1}': {2}", + Level = EventLevel.Informational)] + internal void MtlsFilePermissionSkipped( + string certificateType, + string filePath, + string reason) => this.WriteEvent(31, certificateType, filePath, reason); + + [Event( + 32, Message = "{0} chain validation failed for certificate '{1}'. Errors: {2}", Level = EventLevel.Error)] internal void MtlsCertificateChainValidationFailed( string certificateType, string subject, - string errors) => this.WriteEvent(31, certificateType, subject, errors); + string errors) => this.WriteEvent(32, certificateType, subject, errors); [Event( - 32, + 33, Message = "{0} chain validated successfully for certificate '{1}'.", Level = EventLevel.Informational)] internal void MtlsCertificateChainValidated(string certificateType, string subject) => - this.WriteEvent(32, certificateType, subject); + this.WriteEvent(33, certificateType, subject); [Event( - 33, + 34, Message = "Server certificate validated successfully for '{0}'.", Level = EventLevel.Informational)] - internal void MtlsServerCertificateValidated(string subject) => this.WriteEvent(33, subject); + internal void MtlsServerCertificateValidated(string subject) => this.WriteEvent(34, subject); [Event( - 34, + 35, Message = "Server certificate validation failed for '{0}'. Errors: {1}", Level = EventLevel.Error)] internal void MtlsServerCertificateValidationFailed(string subject, string errors) => - this.WriteEvent(34, subject, errors); + this.WriteEvent(35, subject, errors); [Event( - 35, + 36, Message = "mTLS configuration enabled. Client certificate: '{0}'.", Level = EventLevel.Informational)] internal void MtlsConfigurationEnabled(string clientCertificateSubject) => - this.WriteEvent(35, clientCertificateSubject); + this.WriteEvent(36, clientCertificateSubject); #endif } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 628471f072f..3a75c98c016 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -329,8 +329,14 @@ private static void ValidateFilePermissions(string filePath, string fileType) { ValidateUnixFilePermissions(filePath, fileType); } - - // For other platforms, skip permission validation + else + { + // For other platforms, skip permission validation + OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionSkipped( + fileType, + filePath, + "File permission validation is not supported on this platform."); + } } catch (Exception ex) { From 87633ba011cbefdd5e6d0c2e4f914b83169c4e0c Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 20:54:32 +0900 Subject: [PATCH 17/37] chore(mtls); Remove preemptive file permission validation from mTLS --- ...penTelemetryProtocolExporterEventSource.cs | 45 +---- .../OtlpMtlsCertificateManager.cs | 157 +----------------- .../OtlpMtlsHttpClientFactory.cs | 6 +- .../OtlpMtlsOptions.cs | 6 - .../OtlpMtlsOptionsTests.cs | 3 - 5 files changed, 15 insertions(+), 202 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index ae1fd3b5382..85a7cbdce91 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -258,65 +258,38 @@ internal void MtlsCertificateFileNotFound(string certificateType, string filePat [Event( 29, - Message = "File permission check failed for {0} at '{1}'. Error: {2}", - Level = EventLevel.Warning)] - internal void MtlsFilePermissionCheckFailed( - string certificateType, - string filePath, - string error) => this.WriteEvent(29, certificateType, filePath, error); - - [Event( - 30, - Message = "File permission warning for {0} at '{1}': {2}", - Level = EventLevel.Warning)] - internal void MtlsFilePermissionWarning( - string certificateType, - string filePath, - string warning) => this.WriteEvent(30, certificateType, filePath, warning); - - [Event( - 31, - Message = "File permission validation skipped for {0} at '{1}': {2}", - Level = EventLevel.Informational)] - internal void MtlsFilePermissionSkipped( - string certificateType, - string filePath, - string reason) => this.WriteEvent(31, certificateType, filePath, reason); - - [Event( - 32, Message = "{0} chain validation failed for certificate '{1}'. Errors: {2}", Level = EventLevel.Error)] internal void MtlsCertificateChainValidationFailed( string certificateType, string subject, - string errors) => this.WriteEvent(32, certificateType, subject, errors); + string errors) => this.WriteEvent(29, certificateType, subject, errors); [Event( - 33, + 30, Message = "{0} chain validated successfully for certificate '{1}'.", Level = EventLevel.Informational)] internal void MtlsCertificateChainValidated(string certificateType, string subject) => - this.WriteEvent(33, certificateType, subject); + this.WriteEvent(30, certificateType, subject); [Event( - 34, + 31, Message = "Server certificate validated successfully for '{0}'.", Level = EventLevel.Informational)] - internal void MtlsServerCertificateValidated(string subject) => this.WriteEvent(34, subject); + internal void MtlsServerCertificateValidated(string subject) => this.WriteEvent(31, subject); [Event( - 35, + 32, Message = "Server certificate validation failed for '{0}'. Errors: {1}", Level = EventLevel.Error)] internal void MtlsServerCertificateValidationFailed(string subject, string errors) => - this.WriteEvent(35, subject, errors); + this.WriteEvent(32, subject, errors); [Event( - 36, + 33, Message = "mTLS configuration enabled. Client certificate: '{0}'.", Level = EventLevel.Informational)] internal void MtlsConfigurationEnabled(string clientCertificateSubject) => - this.WriteEvent(36, clientCertificateSubject); + this.WriteEvent(33, clientCertificateSubject); #endif } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 3a75c98c016..96ca333d273 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -4,9 +4,7 @@ #if NET8_0_OR_GREATER using System.Net.Security; -using System.Security.AccessControl; using System.Security.Cryptography.X509Certificates; -using System.Security.Principal; using Microsoft.Extensions.Configuration; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -24,22 +22,13 @@ internal static class OtlpMtlsCertificateManager /// Loads a CA certificate from a PEM file. /// /// Path to the CA certificate file. - /// Whether to check file permissions. /// The loaded CA certificate. /// Thrown when the certificate file is not found. - /// Thrown when file permissions are inadequate. /// Thrown when the certificate cannot be loaded. - public static X509Certificate2 LoadCaCertificate( - string caCertificatePath, - bool enableFilePermissionChecks = true) + public static X509Certificate2 LoadCaCertificate(string caCertificatePath) { ValidateFileExists(caCertificatePath, CaCertificateType); - if (enableFilePermissionChecks) - { - ValidateFilePermissions(caCertificatePath, CaCertificateType); - } - try { var caCertificate = X509Certificate2.CreateFromPemFile(caCertificatePath); @@ -67,17 +56,14 @@ public static X509Certificate2 LoadCaCertificate( /// /// Path to the client certificate file. /// Path to the client private key file. - /// Whether to check file permissions. /// The loaded client certificate with private key. /// Thrown when certificate or key files are not found. - /// Thrown when file permissions are inadequate. /// Thrown when the certificate cannot be loaded. public static X509Certificate2 LoadClientCertificate( string clientCertificatePath, - string clientKeyPath, - bool enableFilePermissionChecks = true) + string clientKeyPath) { - return LoadClientCertificate(clientCertificatePath, clientKeyPath, null, enableFilePermissionChecks); + return LoadClientCertificate(clientCertificatePath, clientKeyPath, null); } /// @@ -86,26 +72,17 @@ public static X509Certificate2 LoadClientCertificate( /// Path to the client certificate file. /// Path to the client private key file. /// Password for the client private key file if it is encrypted. Can be null for unencrypted keys. - /// Whether to check file permissions. /// The loaded client certificate with private key. /// Thrown when certificate or key files are not found. - /// Thrown when file permissions are inadequate. /// Thrown when the certificate cannot be loaded. public static X509Certificate2 LoadClientCertificate( string clientCertificatePath, string clientKeyPath, - string? clientKeyPassword, - bool enableFilePermissionChecks = true) + string? clientKeyPassword) { ValidateFileExists(clientCertificatePath, ClientCertificateType); ValidateFileExists(clientKeyPath, ClientPrivateKeyType); - if (enableFilePermissionChecks) - { - ValidateFilePermissions(clientCertificatePath, ClientCertificateType); - ValidateFilePermissions(clientKeyPath, ClientPrivateKeyType); - } - try { X509Certificate2 clientCertificate; @@ -317,132 +294,6 @@ private static void ValidateFileExists(string filePath, string fileType) } } - private static void ValidateFilePermissions(string filePath, string fileType) - { - try - { - if (OperatingSystem.IsWindows()) - { - ValidateWindowsFilePermissions(filePath, fileType); - } - else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) - { - ValidateUnixFilePermissions(filePath, fileType); - } - else - { - // For other platforms, skip permission validation - OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionSkipped( - fileType, - filePath, - "File permission validation is not supported on this platform."); - } - } - catch (Exception ex) - { - OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionCheckFailed( - fileType, - filePath, - ex.Message); - throw new UnauthorizedAccessException( - $"File permission check failed for {fileType} at '{filePath}': {ex.Message}", - ex); - } - } - - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - private static void ValidateWindowsFilePermissions(string filePath, string fileType) - { - var fileInfo = new FileInfo(filePath); - var fileSecurity = fileInfo.GetAccessControl(); - var accessRules = fileSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); - - var currentUser = WindowsIdentity.GetCurrent(); - bool hasReadAccess = false; - bool hasRestrictedAccess = true; - - foreach (FileSystemAccessRule rule in accessRules) - { - var identity = rule.IdentityReference as SecurityIdentifier; - - // Check if current user has read access - if (identity != null && ( - currentUser.User?.Equals(identity) == true - || currentUser.Groups?.Contains(identity) == true)) - { - if ( - rule.AccessControlType == AccessControlType.Allow - && (rule.FileSystemRights & FileSystemRights.ReadData) != 0) - { - hasReadAccess = true; - } - } - - // Check for overly permissive access (e.g., Everyone, Users group with write access) - if ( - rule.AccessControlType == AccessControlType.Allow - && ( - rule.FileSystemRights - & (FileSystemRights.WriteData | FileSystemRights.FullControl)) != 0) - { - var wellKnownSids = new[] - { - WellKnownSidType.WorldSid, // Everyone - WellKnownSidType.AuthenticatedUserSid, // Authenticated Users - WellKnownSidType.BuiltinUsersSid, // Users - }; - - foreach (var sidType in wellKnownSids) - { - var wellKnownSid = new SecurityIdentifier(sidType, null); - if (identity?.Equals(wellKnownSid) == true) - { - hasRestrictedAccess = false; - break; - } - } - } - } - - if (!hasReadAccess) - { - throw new UnauthorizedAccessException( - $"Current user does not have read access to {fileType} file."); - } - - if (!hasRestrictedAccess) - { - OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionWarning( - fileType, - filePath, - "File has overly permissive access rights. Consider restricting access to improve security."); - } - } - - private static void ValidateUnixFilePermissions(string filePath, string fileType) - { - var fileInfo = new FileInfo(filePath); - - // On Unix systems, we can check if the file is readable by the current user - // by attempting to open it for reading - try - { - using var stream = fileInfo.OpenRead(); - } - catch (UnauthorizedAccessException exception) - { - throw new UnauthorizedAccessException( - $"Current user does not have read access to {fileType} file.", exception); - } - - // For Unix systems, we recommend checking file permissions externally - // as .NET doesn't provide detailed Unix permission APIs - OpenTelemetryProtocolExporterEventSource.Log.MtlsFilePermissionWarning( - fileType, - filePath, - "Consider verifying that file permissions are set to 400 (read-only for owner) for enhanced security."); - } - /// /// Gets the X509RevocationMode from configuration or returns the default value. /// diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 873d17fe921..f4dc268d4dc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -40,8 +40,7 @@ public static HttpClient CreateMtlsHttpClient( if (!string.IsNullOrEmpty(mtlsOptions.CaCertificatePath)) { caCertificate = OtlpMtlsCertificateManager.LoadCaCertificate( - mtlsOptions.CaCertificatePath, - mtlsOptions.EnableFilePermissionChecks); + mtlsOptions.CaCertificatePath); if (mtlsOptions.EnableCertificateChainValidation) { @@ -66,8 +65,7 @@ public static HttpClient CreateMtlsHttpClient( clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate( mtlsOptions.ClientCertificatePath, mtlsOptions.ClientKeyPath, - mtlsOptions.ClientKeyPassword, - mtlsOptions.EnableFilePermissionChecks); + mtlsOptions.ClientKeyPassword); if (mtlsOptions.EnableCertificateChainValidation) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index bc8bca8b0fe..9f7ff585dc4 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -32,12 +32,6 @@ internal class OtlpMtlsOptions /// public string? ClientKeyPassword { get; set; } - /// - /// Gets or sets a value indicating whether to enable file permission checks. - /// When enabled, the exporter will verify that certificate files have appropriate permissions. - /// - public bool EnableFilePermissionChecks { get; set; } = true; - /// /// 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. diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs index f7ab9654be3..bba1886f4b9 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs @@ -17,7 +17,6 @@ public void DefaultValues_AreValid() Assert.Null(options.ClientCertificatePath); Assert.Null(options.ClientKeyPath); Assert.Null(options.CaCertificatePath); - Assert.True(options.EnableFilePermissionChecks); Assert.True(options.EnableCertificateChainValidation); Assert.False(options.IsEnabled); } @@ -31,7 +30,6 @@ public void Properties_CanBeSet() ClientKeyPath = "/path/to/client.key", ClientKeyPassword = "secret123", CaCertificatePath = "/path/to/ca.crt", - EnableFilePermissionChecks = false, EnableCertificateChainValidation = false, }; @@ -39,7 +37,6 @@ public void Properties_CanBeSet() Assert.Equal("/path/to/client.key", options.ClientKeyPath); Assert.Equal("secret123", options.ClientKeyPassword); Assert.Equal("/path/to/ca.crt", options.CaCertificatePath); - Assert.False(options.EnableFilePermissionChecks); Assert.False(options.EnableCertificateChainValidation); Assert.True(options.IsEnabled); } From af26417d32449b382a65fe7299b2656e5c991840 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:28:55 +0900 Subject: [PATCH 18/37] refactor(mtls): optimize mTLS HttpClient creation to reduce allocations --- .../OtlpMtlsCertificateManager.cs | 98 +++++++++---------- .../OtlpMtlsHttpClientFactory.cs | 64 +++--------- .../OtlpExporterOptions.cs | 17 ++-- .../OtlpMtlsHttpClientFactoryTests.cs | 27 +---- 4 files changed, 75 insertions(+), 131 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 96ca333d273..e6c167fb7ce 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -212,68 +212,68 @@ public static bool ValidateCertificateChain( } /// - /// Creates a server certificate validation callback that uses a custom CA certificate. + /// Validates a server certificate against the provided CA certificate. /// - /// The CA certificate to use for validation. - /// A validation callback function. - public static Func< - X509Certificate2, - X509Chain, - SslPolicyErrors, - bool> CreateServerCertificateValidationCallback(X509Certificate2 caCertificate) + /// 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) { - return (serverCert, chain, sslPolicyErrors) => + try { - try + // If there are no SSL policy errors, accept the certificate + if (sslPolicyErrors == SslPolicyErrors.None) { - // If there are no SSL policy errors, accept the certificate - if (sslPolicyErrors == SslPolicyErrors.None) - { - return true; - } + 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; + // 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); + bool isValid = chain.Build(serverCert); - if (isValid) + 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)) { - // 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.MtlsServerCertificateValidated( + serverCert.Subject); + return true; } } + } - OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed( - serverCert.Subject, - sslPolicyErrors.ToString()); + OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed( + serverCert.Subject, + sslPolicyErrors.ToString()); - return false; - } - catch (Exception ex) - { - OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed( - serverCert.Subject, - ex.Message); + return false; + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.MtlsServerCertificateValidationFailed( + serverCert.Subject, + ex.Message); - return false; - } - }; + return false; + } } private static void ValidateFileExists(string filePath, string fileType) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index f4dc268d4dc..3e9f4fdbba2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -16,18 +16,19 @@ internal static class OtlpMtlsHttpClientFactory /// Creates an HttpClient configured with mTLS settings. /// /// The mTLS configuration options. - /// The base HttpClient factory to use. + /// Optional action to configure the client. /// An HttpClient configured for mTLS. public static HttpClient CreateMtlsHttpClient( OtlpMtlsOptions mtlsOptions, - Func baseFactory) + Action? configureClient = null) { ArgumentNullException.ThrowIfNull(mtlsOptions); - ArgumentNullException.ThrowIfNull(baseFactory); if (!mtlsOptions.IsEnabled) { - return baseFactory(); + var client = new HttpClient(); + configureClient?.Invoke(client); + return client; } HttpClientHandler? handler = null; @@ -105,10 +106,8 @@ public static HttpClient CreateMtlsHttpClient( return false; } - var serverCertValidationCallback = - OtlpMtlsCertificateManager.CreateServerCertificateValidationCallback( - caCertificate); - return serverCertValidationCallback(cert, chain, sslPolicyErrors); + return OtlpMtlsCertificateManager.ValidateServerCertificate( + cert, chain, sslPolicyErrors, caCertificate); }; } else if (mtlsOptions.ServerCertificateValidationCallback != null) @@ -133,7 +132,7 @@ public static HttpClient CreateMtlsHttpClient( var client = new HttpClient(handler, disposeHandler: true); - ConfigureHttpClientDefaults(client, baseFactory); + configureClient?.Invoke(client); // Dispose certificates as they are no longer needed after being added to the handler caCertificate?.Dispose(); @@ -150,47 +149,14 @@ public static HttpClient CreateMtlsHttpClient( OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); throw; + +<<<<<<< TODO: プロジェクト 'OpenTelemetry.Exporter.OpenTelemetryProtocol(net8.0)' からのマージされていない変更, 前: + }} +======= + } + } +>>>>>>> 後 } } - /// - /// Creates an HttpClient factory that supports mTLS configuration. - /// - /// The mTLS configuration options. - /// The base HttpClient factory to use. - /// A factory function that creates mTLS-configured HttpClient instances. - public static Func CreateMtlsHttpClientFactory( - OtlpMtlsOptions? mtlsOptions, - Func baseFactory) - { - if (mtlsOptions == null || !mtlsOptions.IsEnabled) - { - return baseFactory; - } - - return () => CreateMtlsHttpClient(mtlsOptions, baseFactory); - } - - /// - /// Configures the HttpClient with default settings from the base factory. - /// - /// The HttpClient to configure. - /// The base HttpClient factory to get settings from. - private static void ConfigureHttpClientDefaults(HttpClient client, Func baseFactory) - { - // Get base HttpClient to copy settings - using var baseClient = baseFactory(); - - // Copy settings from base client - client.Timeout = baseClient.Timeout; - client.BaseAddress = baseClient.BaseAddress; - - // Copy default headers - foreach (var header in baseClient.DefaultRequestHeaders) - { - client.DefaultRequestHeaders.Add(header.Key, header.Value); - } - } -} - #endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index e70e43ef7d7..2489a06f8fc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -72,10 +72,7 @@ internal OtlpExporterOptions( this.DefaultHttpClientFactory = () => { - var baseClient = new HttpClient - { - Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), - }; + var timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds); #if NET8_0_OR_GREATER // If mTLS is configured, create an mTLS-enabled client @@ -83,17 +80,15 @@ internal OtlpExporterOptions( { var mtlsClient = OtlpMtlsHttpClientFactory.CreateMtlsHttpClient( this.MtlsOptions, - () => - new HttpClient - { - Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), - }); - baseClient.Dispose(); + client => client.Timeout = timeout); return mtlsClient; } #endif - return baseClient; + return new HttpClient + { + Timeout = timeout, + }; }; this.BatchExportProcessorOptions = defaultBatchOptions!; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs index 4effd37827f..eda859d6d38 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs @@ -10,10 +10,9 @@ public class OtlpMtlsHttpClientFactoryTests [Xunit.Fact] public void CreateHttpClient_ReturnsHttpClient_WhenMtlsIsDisabled() { - var baseFactory = () => new HttpClient(); var options = new OtlpMtlsOptions(); // Disabled by default - using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory); + using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options); Xunit.Assert.NotNull(httpClient); Xunit.Assert.IsType(httpClient); @@ -22,11 +21,10 @@ public void CreateHttpClient_ReturnsHttpClient_WhenMtlsIsDisabled() [Xunit.Fact] public void CreateHttpClient_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist() { - var baseFactory = () => new HttpClient(); var options = new OtlpMtlsOptions { ClientCertificatePath = "/nonexistent/client.crt" }; var exception = Xunit.Assert.Throws(() => - OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory)); + OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options)); Xunit.Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); } @@ -42,7 +40,6 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro var certBytes = cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx, "testpassword"); File.WriteAllBytes(tempCertFile, certBytes); - var baseFactory = () => new HttpClient(); var options = new OtlpMtlsOptions { ClientCertificatePath = tempCertFile, @@ -51,7 +48,7 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro EnableCertificateChainValidation = false, // Ignore validation for test cert }; - using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory); + using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options); Xunit.Assert.NotNull(httpClient); @@ -86,13 +83,12 @@ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRo $"-----BEGIN CERTIFICATE-----\n{trustedCertPem}\n-----END CERTIFICATE-----"; File.WriteAllText(tempTrustStoreFile, pemContent); - var baseFactory = () => new HttpClient(); var options = new OtlpMtlsOptions { CaCertificatePath = tempTrustStoreFile, }; - using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, baseFactory); + using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options); Xunit.Assert.NotNull(httpClient); @@ -114,24 +110,11 @@ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRo } } - [Xunit.Fact] - public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenBaseFactoryIsNull() - { - var options = new OtlpMtlsOptions(); - - var exception = Xunit.Assert.Throws(() => - OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options, null!)); - - Xunit.Assert.Equal("baseFactory", exception.ParamName); - } - [Xunit.Fact] public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenOptionsIsNull() { - var baseFactory = () => new HttpClient(); - var exception = Xunit.Assert.Throws(() => - OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(null!, baseFactory)); + OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(null!)); Xunit.Assert.Equal("mtlsOptions", exception.ParamName); } From 0bd7c5e4f202ff98edd9a09d34a9f7a386ad7649 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:31:49 +0900 Subject: [PATCH 19/37] fix(mtls): format issue --- .../Implementation/OtlpMtlsHttpClientFactory.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 3e9f4fdbba2..195f750a950 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -149,14 +149,9 @@ public static HttpClient CreateMtlsHttpClient( OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); throw; - -<<<<<<< TODO: プロジェクト 'OpenTelemetry.Exporter.OpenTelemetryProtocol(net8.0)' からのマージされていない変更, 前: - }} -======= - } - } ->>>>>>> 後 + } } +} #endif From 487d5175d2bdc215aa2ae404d1a04a3ef9dab94c Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:32:57 +0900 Subject: [PATCH 20/37] fix(mtls): format issue --- .../Implementation/OtlpMtlsHttpClientFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 195f750a950..cc62163dde1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -149,7 +149,6 @@ public static HttpClient CreateMtlsHttpClient( OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); throw; - } } } From 59cea03740755c11050dccbd344a5ffd059b184a Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:34:19 +0900 Subject: [PATCH 21/37] chore(mtls): Make OtlpMtlsOptions sealed --- .../OtlpMtlsOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 9f7ff585dc4..04b186864e5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -8,7 +8,7 @@ namespace OpenTelemetry.Exporter; -internal class OtlpMtlsOptions +internal sealed class OtlpMtlsOptions { /// /// Gets or sets the path to the CA certificate file in PEM format. From b80cd2119d68ac4f5ac0f3c1f908e9a9e0ffaed7 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:37:32 +0900 Subject: [PATCH 22/37] fix(mtls): format issue --- .../OtlpMtlsHttpClientFactory.cs | 19 ------------------- .../OtlpMtlsOptions.cs | 13 ------------- 2 files changed, 32 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index cc62163dde1..a30952b850a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -110,25 +110,6 @@ public static HttpClient CreateMtlsHttpClient( cert, chain, sslPolicyErrors, caCertificate); }; } - else if (mtlsOptions.ServerCertificateValidationCallback != null) - { - handler.ServerCertificateCustomValidationCallback = ( - httpRequestMessage, - cert, - chain, - sslPolicyErrors) => - { - if (cert == null || chain == null) - { - return false; - } - - return mtlsOptions.ServerCertificateValidationCallback( - cert, - chain, - sslPolicyErrors); - }; - } var client = new HttpClient(handler, disposeHandler: true); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 04b186864e5..05342603550 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -38,19 +38,6 @@ internal sealed class OtlpMtlsOptions /// public bool EnableCertificateChainValidation { get; set; } = true; - /// - /// Gets or sets the server certificate validation callback. - /// This callback is used to validate the server certificate during TLS handshake. - /// If not set, the default certificate validation logic will be used. - /// - public Func< - X509Certificate2, - X509Chain, - SslPolicyErrors, - bool - >? ServerCertificateValidationCallback - { get; set; } - /// /// Gets a value indicating whether mTLS is enabled. /// mTLS is considered enabled if at least the client certificate path is provided. From 6e78148575b8c74e8c4d034eeab564f962ed7f5d Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:39:04 +0900 Subject: [PATCH 23/37] fix(mtls): format issue --- .../OtlpMtlsOptions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 05342603550..45458fa8b2a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -3,9 +3,6 @@ #if NET8_0_OR_GREATER -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - namespace OpenTelemetry.Exporter; internal sealed class OtlpMtlsOptions From 0fca40bfb184e0e8507675e3645fedf2251cf094 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:40:54 +0900 Subject: [PATCH 24/37] chore(README): table inline --- .../README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index dbe284b52b6..5c9750e76c0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -453,14 +453,14 @@ or reader 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)| - | `OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD` | `ClientKeyPassword` | Password for encrypted client private key file (PEM)| - | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` | - | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` | + | 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)| + | `OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD` | `ClientKeyPassword` | Password for encrypted client private key file (PEM)| + | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` | + | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` | * Logs: From fc25a731108a81f7ee5ff20523657be100e0bd3b Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Tue, 24 Jun 2025 21:43:00 +0900 Subject: [PATCH 25/37] fix(): clean up shipped file --- .../.publicApi/Stable/PublicAPI.Unshipped.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt index 8b137891791..e69de29bb2d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -1 +0,0 @@ - From c8a5a55f9e8ff74645e84edf3c582c422cd7d2c4 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:38:18 +0900 Subject: [PATCH 26/37] chore(mtls): fix README doc Co-authored-by: Martin Costello --- .../README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 5c9750e76c0..85b104899e1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -453,12 +453,12 @@ or reader 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)| - | `OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD` | `ClientKeyPassword` | Password for encrypted client private key file (PEM)| + | 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) | + | `OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD` | `ClientKeyPassword` | Password for encrypted client private key file (PEM) | | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` | | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` | From 464201be5ace6bf731db797382df7d9d4d855b22 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:40:15 +0900 Subject: [PATCH 27/37] chore(mtls): adjust return mtlsClient to 1-liner Co-authored-by: Martin Costello --- .../OtlpExporterOptions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 2489a06f8fc..0c229c92461 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -78,10 +78,9 @@ internal OtlpExporterOptions( // If mTLS is configured, create an mTLS-enabled client if (this.MtlsOptions?.IsEnabled == true) { - var mtlsClient = OtlpMtlsHttpClientFactory.CreateMtlsHttpClient( + return OtlpMtlsHttpClientFactory.CreateMtlsHttpClient( this.MtlsOptions, client => client.Timeout = timeout); - return mtlsClient; } #endif From ec0db21400d4ed64d2d4d881372130e73ae51034 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:43:41 +0900 Subject: [PATCH 28/37] chore(mtls): simplify new OtlpMtlsOptions() --- .../OtlpExporterOptions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 0c229c92461..fb624fd59cc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -316,28 +316,28 @@ private void ApplyMtlsConfiguration(IConfiguration configuration) // Check and apply CA certificate path from environment variable if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateEnvVarName, out var caCertPath)) { - this.MtlsOptions ??= new OtlpMtlsOptions(); + 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 OtlpMtlsOptions(); + 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 OtlpMtlsOptions(); + this.MtlsOptions ??= new(); this.MtlsOptions.ClientKeyPath = clientKeyPath; } // Check and apply client key password from environment variable if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName, out var clientKeyPassword)) { - this.MtlsOptions ??= new OtlpMtlsOptions(); + this.MtlsOptions ??= new(); this.MtlsOptions.ClientKeyPassword = clientKeyPassword; } } From 84764e834cccff45fed2e8a3414b168692bb3773 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:45:13 +0900 Subject: [PATCH 29/37] refactor(mtls): simplify null check and add comment for ExcludeRoot default --- .../Implementation/OtlpMtlsCertificateManager.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index e6c167fb7ce..4c3539b594b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -328,12 +328,7 @@ private static X509RevocationMode GetRevocationModeFromConfiguration(IConfigurat /// The configured revocation flag or default (ExcludeRoot). private static X509RevocationFlag GetRevocationFlagFromConfiguration(IConfiguration? configuration) { - if (configuration == null) - { - return X509RevocationFlag.ExcludeRoot; - } - - if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, out var flagString)) + if (configuration != null && configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, out var flagString)) { if (Enum.TryParse(flagString, true, out var flag)) { @@ -345,6 +340,8 @@ private static X509RevocationFlag GetRevocationFlagFromConfiguration(IConfigurat flagString); } + // Use ExcludeRoot as default to avoid revocation checks on the root CA certificate, + // which is typically self-signed and may not have revocation information available return X509RevocationFlag.ExcludeRoot; } } From 3ec30f9c5e9cadace814fe50b1537ef4c664a5f6 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:47:06 +0900 Subject: [PATCH 30/37] fix(mtls): Fix client certificate loading when ClientKeyPath is not provided --- .../OtlpMtlsHttpClientFactory.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index a30952b850a..79e8dd20385 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -55,11 +55,11 @@ public static HttpClient CreateMtlsHttpClient( { if (string.IsNullOrEmpty(mtlsOptions.ClientKeyPath)) { - // Check if certificate file exists to provide appropriate error message - if (!File.Exists(mtlsOptions.ClientCertificatePath)) - { - throw new FileNotFoundException($"Certificate file not found at path: {mtlsOptions.ClientCertificatePath}", mtlsOptions.ClientCertificatePath); - } + // Load certificate without separate key file (e.g., PKCS#12 format) + clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate( + mtlsOptions.ClientCertificatePath, + null, + mtlsOptions.ClientKeyPassword); } else { @@ -67,17 +67,17 @@ public static HttpClient CreateMtlsHttpClient( mtlsOptions.ClientCertificatePath, mtlsOptions.ClientKeyPath, mtlsOptions.ClientKeyPassword); + } - if (mtlsOptions.EnableCertificateChainValidation) - { - OtlpMtlsCertificateManager.ValidateCertificateChain( - clientCertificate, - OtlpMtlsCertificateManager.ClientCertificateType); - } - - OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled( - clientCertificate.Subject); + if (mtlsOptions.EnableCertificateChainValidation) + { + OtlpMtlsCertificateManager.ValidateCertificateChain( + clientCertificate, + OtlpMtlsCertificateManager.ClientCertificateType); } + + OpenTelemetryProtocolExporterEventSource.Log.MtlsConfigurationEnabled( + clientCertificate.Subject); } // Create HttpClientHandler with mTLS configuration From f30b6f865e439d8bf212a03f6c8a18ed11fbb3a8 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:49:03 +0900 Subject: [PATCH 31/37] chore(mtls): Improve certificate disposal in OtlpMtlsHttpClientFactory --- .../Implementation/OtlpMtlsHttpClientFactory.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 79e8dd20385..23d91d27bd2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -115,22 +115,22 @@ public static HttpClient CreateMtlsHttpClient( configureClient?.Invoke(client); - // Dispose certificates as they are no longer needed after being added to the handler - caCertificate?.Dispose(); - clientCertificate?.Dispose(); - return client; } catch (Exception ex) { - // Dispose resources if something went wrong + // Dispose handler if something went wrong handler?.Dispose(); - caCertificate?.Dispose(); - clientCertificate?.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(); + } } } From b948ce18a1f0ba7d73db7b0c44917f5a946bbd8c Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 18:54:51 +0900 Subject: [PATCH 32/37] fix(mtls): Add missing using Xunit statements to mTLS test files --- .../OtlpMtlsCertificateManagerTests.cs | 107 +++++++++--------- .../OtlpMtlsHttpClientFactoryTests.cs | 32 +++--- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs index c4532ed5aae..127ab683ca1 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs @@ -4,6 +4,7 @@ #if NET8_0_OR_GREATER using Microsoft.Extensions.Configuration; +using Xunit; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; @@ -32,19 +33,19 @@ public class OtlpMtlsCertificateManagerTests 4567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCD -----END CERTIFICATE-----"; - [Xunit.Fact] + [Fact] public void LoadClientCertificate_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist() { - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate( "/nonexistent/client.crt", "/nonexistent/client.key")); - Xunit.Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); - Xunit.Assert.Contains("/nonexistent/client.crt", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("/nonexistent/client.crt", exception.Message, StringComparison.OrdinalIgnoreCase); } - [Xunit.Fact] + [Fact] public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFileDoesNotExist() { var tempCertFile = Path.GetTempFileName(); @@ -52,13 +53,13 @@ public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFile try { - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate( tempCertFile, "/nonexistent/client.key")); - Xunit.Assert.Contains("Private key file not found", exception.Message, StringComparison.OrdinalIgnoreCase); - Xunit.Assert.Contains("/nonexistent/client.key", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Private key file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("/nonexistent/client.key", exception.Message, StringComparison.OrdinalIgnoreCase); } finally { @@ -66,17 +67,17 @@ public void LoadClientCertificate_ThrowsFileNotFoundException_WhenPrivateKeyFile } } - [Xunit.Fact] + [Fact] public void LoadCaCertificate_ThrowsFileNotFoundException_WhenTrustStoreFileDoesNotExist() { - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate("/nonexistent/ca.crt")); - Xunit.Assert.Contains("CA certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); - Xunit.Assert.Contains("/nonexistent/ca.crt", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("CA certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("/nonexistent/ca.crt", exception.Message, StringComparison.OrdinalIgnoreCase); } - [Xunit.Fact] + [Fact] public void LoadClientCertificate_ThrowsInvalidOperationException_WhenCertificateFileIsEmpty() { var tempCertFile = Path.GetTempFileName(); @@ -86,10 +87,10 @@ public void LoadClientCertificate_ThrowsInvalidOperationException_WhenCertificat try { - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate(tempCertFile, tempKeyFile)); - Xunit.Assert.Contains( + Assert.Contains( "Failed to load client certificate", exception.Message, StringComparison.OrdinalIgnoreCase); @@ -101,7 +102,7 @@ public void LoadClientCertificate_ThrowsInvalidOperationException_WhenCertificat } } - [Xunit.Fact] + [Fact] public void LoadCaCertificate_ThrowsInvalidOperationException_WhenTrustStoreFileIsEmpty() { var tempTrustStoreFile = Path.GetTempFileName(); @@ -109,10 +110,10 @@ public void LoadCaCertificate_ThrowsInvalidOperationException_WhenTrustStoreFile try { - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadCaCertificate(tempTrustStoreFile)); - Xunit.Assert.Contains( + Assert.Contains( "Failed to load CA certificate", exception.Message, StringComparison.OrdinalIgnoreCase); @@ -123,7 +124,7 @@ public void LoadCaCertificate_ThrowsInvalidOperationException_WhenTrustStoreFile } } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_DoesNotThrow_WithValidCertificate() { // Create a self-signed certificate for testing @@ -133,10 +134,10 @@ public void ValidateCertificateChain_DoesNotThrow_WithValidCertificate() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate"); // For self-signed certificates, validation may fail, but method should not throw - Xunit.Assert.True(result || !result); // Just check that it returns a boolean + Assert.True(result || !result); // Just check that it returns a boolean } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_ReturnsResult_WithValidCertificate() { // Create a valid certificate for testing @@ -146,10 +147,10 @@ public void ValidateCertificateChain_ReturnsResult_WithValidCertificate() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate"); // The result can be true or false, but the method should not throw - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_UsesDefaultConfiguration_WhenConfigurationIsNull() { using var cert = CreateSelfSignedCertificate(); @@ -159,10 +160,10 @@ public void ValidateCertificateChain_UsesDefaultConfiguration_WhenConfigurationI var result2 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", null); // Results should be the same since both use defaults - Xunit.Assert.Equal(result1, result2); + Assert.Equal(result1, result2); } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_UsesRevocationModeFromConfiguration() { using var cert = CreateSelfSignedCertificate(); @@ -178,10 +179,10 @@ public void ValidateCertificateChain_UsesRevocationModeFromConfiguration() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_UsesRevocationFlagFromConfiguration() { using var cert = CreateSelfSignedCertificate(); @@ -197,10 +198,10 @@ public void ValidateCertificateChain_UsesRevocationFlagFromConfiguration() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_UsesBothRevocationConfigurationValues() { using var cert = CreateSelfSignedCertificate(); @@ -217,10 +218,10 @@ public void ValidateCertificateChain_UsesBothRevocationConfigurationValues() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationMode() { using var cert = CreateSelfSignedCertificate(); @@ -236,10 +237,10 @@ public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationMode() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing and use default Online mode - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Fact] + [Fact] public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationFlag() { using var cert = CreateSelfSignedCertificate(); @@ -255,16 +256,16 @@ public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationFlag() var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing and use default ExcludeRoot flag - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Theory] - [Xunit.InlineData("Online")] - [Xunit.InlineData("Offline")] - [Xunit.InlineData("NoCheck")] - [Xunit.InlineData("online")] - [Xunit.InlineData("OFFLINE")] - [Xunit.InlineData("nocheck")] + [Theory] + [InlineData("Online")] + [InlineData("Offline")] + [InlineData("NoCheck")] + [InlineData("online")] + [InlineData("OFFLINE")] + [InlineData("nocheck")] public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationMode(string revocationMode) { using var cert = CreateSelfSignedCertificate(); @@ -280,16 +281,16 @@ public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationMode(string var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Theory] - [Xunit.InlineData("ExcludeRoot")] - [Xunit.InlineData("EntireChain")] - [Xunit.InlineData("EndCertificateOnly")] - [Xunit.InlineData("excluderoot")] - [Xunit.InlineData("ENTIRECHAIN")] - [Xunit.InlineData("endcertificateonly")] + [Theory] + [InlineData("ExcludeRoot")] + [InlineData("EntireChain")] + [InlineData("EndCertificateOnly")] + [InlineData("excluderoot")] + [InlineData("ENTIRECHAIN")] + [InlineData("endcertificateonly")] public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationFlag(string revocationFlag) { using var cert = CreateSelfSignedCertificate(); @@ -305,10 +306,10 @@ public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationFlag(string var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); // The method should execute without throwing - Xunit.Assert.True(result || !result); + Assert.True(result || !result); } - [Xunit.Fact] + [Fact] public void LoadClientCertificate_AcceptsPasswordParameter() { var tempCertFile = Path.GetTempFileName(); @@ -321,14 +322,14 @@ public void LoadClientCertificate_AcceptsPasswordParameter() // This test verifies that the method accepts the password parameter // 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 = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.LoadClientCertificate( tempCertFile, tempKeyFile, "test-password")); // The exception should be about certificate loading, not method signature - Xunit.Assert.Contains("Failed to load client certificate", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Failed to load client certificate", exception.Message, StringComparison.OrdinalIgnoreCase); } finally { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs index eda859d6d38..550b58e7677 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs @@ -3,33 +3,35 @@ #if NET8_0_OR_GREATER +using Xunit; + namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpMtlsHttpClientFactoryTests { - [Xunit.Fact] + [Fact] public void CreateHttpClient_ReturnsHttpClient_WhenMtlsIsDisabled() { var options = new OtlpMtlsOptions(); // Disabled by default using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options); - Xunit.Assert.NotNull(httpClient); - Xunit.Assert.IsType(httpClient); + Assert.NotNull(httpClient); + Assert.IsType(httpClient); } - [Xunit.Fact] + [Fact] public void CreateHttpClient_ThrowsFileNotFoundException_WhenCertificateFileDoesNotExist() { var options = new OtlpMtlsOptions { ClientCertificatePath = "/nonexistent/client.crt" }; - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options)); - Xunit.Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Certificate file not found", exception.Message, StringComparison.OrdinalIgnoreCase); } - [Xunit.Fact] + [Fact] public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificateProvided() { var tempCertFile = Path.GetTempFileName(); @@ -50,7 +52,7 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options); - Xunit.Assert.NotNull(httpClient); + Assert.NotNull(httpClient); // Verify the HttpClientHandler has client certificates configured var handlerField = typeof(HttpClient).GetField( @@ -58,7 +60,7 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (handlerField?.GetValue(httpClient) is HttpClientHandler handler) { - Xunit.Assert.NotEmpty(handler.ClientCertificates); + Assert.NotEmpty(handler.ClientCertificates); } } finally @@ -70,7 +72,7 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro } } - [Xunit.Fact] + [Fact] public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRootCertificatesProvided() { var tempTrustStoreFile = Path.GetTempFileName(); @@ -90,7 +92,7 @@ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRo using var httpClient = OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(options); - Xunit.Assert.NotNull(httpClient); + Assert.NotNull(httpClient); // Verify the HttpClientHandler has server certificate validation configured var handlerField = typeof(HttpClient).GetField( @@ -98,7 +100,7 @@ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRo System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (handlerField?.GetValue(httpClient) is HttpClientHandler handler) { - Xunit.Assert.NotNull(handler.ServerCertificateCustomValidationCallback); + Assert.NotNull(handler.ServerCertificateCustomValidationCallback); } } finally @@ -110,13 +112,13 @@ public void CreateHttpClient_ConfiguresServerCertificateValidation_WhenTrustedRo } } - [Xunit.Fact] + [Fact] public void CreateMtlsHttpClient_ThrowsArgumentNullException_WhenOptionsIsNull() { - var exception = Xunit.Assert.Throws(() => + var exception = Assert.Throws(() => OpenTelemetryProtocol.Implementation.OtlpMtlsHttpClientFactory.CreateMtlsHttpClient(null!)); - Xunit.Assert.Equal("mtlsOptions", exception.ParamName); + Assert.Equal("mtlsOptions", exception.ParamName); } private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateSelfSignedCertificate() From 3d756946ca5f9d252764e22dcf8cfe51ef25817e Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 19:02:48 +0900 Subject: [PATCH 33/37] fix(mtls): suppress SYSLIB0057 warnings in certificate loading code --- .../OtlpMtlsCertificateManager.cs | 77 +++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 4c3539b594b..73118eac7d3 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -67,19 +67,86 @@ public static X509Certificate2 LoadClientCertificate( } /// - /// Loads a client certificate with its private key from PEM files. + /// Loads a client certificate from a single file (e.g., PKCS#12 format) with optional password. /// /// Path to the client certificate file. - /// Path to the client private key file. - /// Password for the client private key file if it is encrypted. Can be null for unencrypted keys. + /// Must be null for single-file certificates. + /// Password for the certificate file if it is encrypted. Can be null for unencrypted certificates. /// The loaded client certificate with private key. - /// Thrown when certificate or key files are not found. + /// 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, + string? clientKeyPath, string? clientKeyPassword) { + 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 + if (!string.IsNullOrEmpty(clientKeyPassword)) + { + // Load PKCS#12 with password +#if NET9_0_OR_GREATER + clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, clientKeyPassword); +#else +#pragma warning disable SYSLIB0057 // X509Certificate2 constructors are obsolete. Use X509CertificateLoader instead. + clientCertificate = new X509Certificate2(clientCertificatePath, clientKeyPassword); +#pragma warning restore SYSLIB0057 +#endif + } + else + { + // Try PKCS#12 without password first + try + { +#if NET9_0_OR_GREATER + clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, (string?)null); +#else +#pragma warning disable SYSLIB0057 // X509Certificate2 constructors are obsolete. Use X509CertificateLoader instead. + clientCertificate = new X509Certificate2(clientCertificatePath); +#pragma warning restore SYSLIB0057 +#endif + } + catch + { + // 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); From 4d8b83ecb5c9d6ca7709b26e40ce7eb9644955bb Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 19:13:15 +0900 Subject: [PATCH 34/37] fix(mtls): fix test --- .../OtlpMtlsHttpClientFactoryTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs index 550b58e7677..b8298cf41ae 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs @@ -45,8 +45,7 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro var options = new OtlpMtlsOptions { ClientCertificatePath = tempCertFile, - - // Note: Password support would need to be added to OtlpMtlsOptions + ClientKeyPassword = "testpassword", EnableCertificateChainValidation = false, // Ignore validation for test cert }; From 36111450b3a28677417335a3d3420b92d73441a7 Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 19:47:19 +0900 Subject: [PATCH 35/37] chore(): replace #if NET --- .../OpenTelemetryProtocolExporterEventSource.cs | 2 +- .../Implementation/OtlpMtlsCertificateManager.cs | 2 +- .../Implementation/OtlpMtlsHttpClientFactory.cs | 2 +- .../OtlpExporterOptions.cs | 8 ++++---- .../OtlpMtlsOptions.cs | 2 +- .../OtlpExporterOptionsTests.cs | 2 +- .../OtlpMtlsCertificateManagerTests.cs | 2 +- .../OtlpMtlsHttpClientFactoryTests.cs | 2 +- .../OtlpMtlsOptionsTests.cs | 2 +- .../OtlpSpecConfigDefinitionsTests.cs | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 85a7cbdce91..137c1c718c1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -241,7 +241,7 @@ void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, str this.InvalidConfigurationValue(key, value); } -#if NET8_0_OR_GREATER +#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); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 73118eac7d3..bd0595b31b1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using System.Net.Security; using System.Security.Cryptography.X509Certificates; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 23d91d27bd2..6710522661f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using System.Security.Cryptography.X509Certificates; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index fb624fd59cc..86c6a702d63 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -74,7 +74,7 @@ internal OtlpExporterOptions( { var timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds); -#if NET8_0_OR_GREATER +#if NET // If mTLS is configured, create an mTLS-enabled client if (this.MtlsOptions?.IsEnabled == true) { @@ -170,7 +170,7 @@ public Func HttpClientFactory /// internal bool AppendSignalPathToEndpoint { get; private set; } = true; -#if NET8_0_OR_GREATER +#if NET internal OtlpMtlsOptions? MtlsOptions { get; set; } #endif @@ -302,13 +302,13 @@ private void ApplyConfiguration( throw new NotSupportedException($"OtlpExporterOptionsConfigurationType '{configurationType}' is not supported."); } -#if NET8_0_OR_GREATER +#if NET // Apply mTLS configuration from environment variables this.ApplyMtlsConfiguration(configuration); #endif } -#if NET8_0_OR_GREATER +#if NET private void ApplyMtlsConfiguration(IConfiguration configuration) { Debug.Assert(configuration != null, "configuration was null"); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 45458fa8b2a..763cb43a8a0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET namespace OpenTelemetry.Exporter; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 26668244c29..8b03b3cd92a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -264,7 +264,7 @@ public void OtlpExporterOptions_ApplyDefaultsTest() Assert.NotEqual(defaultOptionsWithData.HttpClientFactory, targetOptionsWithData.HttpClientFactory); } -#if NET8_0_OR_GREATER +#if NET [Fact] public void OtlpExporterOptions_MtlsEnvironmentVariables() { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs index 127ab683ca1..e15ef1628fc 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using Microsoft.Extensions.Configuration; using Xunit; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs index b8298cf41ae..689e183f630 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using Xunit; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs index bba1886f4b9..b96dabb3a9a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using Xunit; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs index fa5a5844318..52ebf4b621e 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET8_0_OR_GREATER +#if NET using Xunit; From 3f7a6bb3ce3c96220955164ca7635c9662a0022a Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 19:49:53 +0900 Subject: [PATCH 36/37] chore(mtls): remove needless pragma warning restore SYSLIB0057 --- .../Implementation/OtlpMtlsCertificateManager.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index bd0595b31b1..66d9ca09932 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -97,9 +97,7 @@ public static X509Certificate2 LoadClientCertificate( #if NET9_0_OR_GREATER clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, clientKeyPassword); #else -#pragma warning disable SYSLIB0057 // X509Certificate2 constructors are obsolete. Use X509CertificateLoader instead. clientCertificate = new X509Certificate2(clientCertificatePath, clientKeyPassword); -#pragma warning restore SYSLIB0057 #endif } else @@ -110,9 +108,7 @@ public static X509Certificate2 LoadClientCertificate( #if NET9_0_OR_GREATER clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, (string?)null); #else -#pragma warning disable SYSLIB0057 // X509Certificate2 constructors are obsolete. Use X509CertificateLoader instead. clientCertificate = new X509Certificate2(clientCertificatePath); -#pragma warning restore SYSLIB0057 #endif } catch From 6135c336b5aff7b51d1fbdecca56a119b0674b1c Mon Sep 17 00:00:00 2001 From: Sandy Chen Date: Wed, 25 Jun 2025 19:51:18 +0900 Subject: [PATCH 37/37] fix(): Remove support for client key password and certificate revocation configuration --- .../OtlpMtlsCertificateManager.cs | 145 ++------------- .../OtlpMtlsHttpClientFactory.cs | 6 +- .../OtlpSpecConfigDefinitions.cs | 5 - .../OtlpExporterOptions.cs | 7 - .../OtlpMtlsOptions.cs | 7 - .../README.md | 3 - .../OtlpExporterOptionsTests.cs | 26 --- .../OtlpMtlsCertificateManagerTests.cs | 167 +----------------- .../OtlpMtlsHttpClientFactoryTests.cs | 3 +- .../OtlpMtlsOptionsTests.cs | 2 - .../OtlpSpecConfigDefinitionsTests.cs | 29 --- 11 files changed, 21 insertions(+), 379 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs index 66d9ca09932..4f358142a74 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsCertificateManager.cs @@ -4,8 +4,8 @@ #if NET using System.Net.Security; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Configuration; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; @@ -52,34 +52,17 @@ public static X509Certificate2 LoadCaCertificate(string caCertificatePath) } /// - /// Loads a client certificate with its private key from PEM files. + /// 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. - /// The loaded client certificate with private key. - /// Thrown when certificate or key files are not found. - /// Thrown when the certificate cannot be loaded. - public static X509Certificate2 LoadClientCertificate( - string clientCertificatePath, - string clientKeyPath) - { - return LoadClientCertificate(clientCertificatePath, clientKeyPath, null); - } - - /// - /// Loads a client certificate from a single file (e.g., PKCS#12 format) with optional password. - /// - /// Path to the client certificate file. - /// Must be null for single-file certificates. - /// Password for the certificate file if it is encrypted. Can be null for unencrypted certificates. + /// 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, - string? clientKeyPassword) + string? clientKeyPath) { if (clientKeyPath == null) { @@ -91,31 +74,18 @@ public static X509Certificate2 LoadClientCertificate( X509Certificate2 clientCertificate; // Try to load as PKCS#12 first, then as PEM - if (!string.IsNullOrEmpty(clientKeyPassword)) + try { - // Load PKCS#12 with password #if NET9_0_OR_GREATER - clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, clientKeyPassword); + clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, (string?)null); #else - clientCertificate = new X509Certificate2(clientCertificatePath, clientKeyPassword); + clientCertificate = new X509Certificate2(clientCertificatePath); #endif } - else + catch (Exception ex) when (ex is CryptographicException || ex is InvalidDataException || ex is FormatException) { - // Try PKCS#12 without password first - try - { -#if NET9_0_OR_GREATER - clientCertificate = X509CertificateLoader.LoadPkcs12FromFile(clientCertificatePath, (string?)null); -#else - clientCertificate = new X509Certificate2(clientCertificatePath); -#endif - } - catch - { - // If PKCS#12 fails, try PEM format - clientCertificate = X509Certificate2.CreateFromPemFile(clientCertificatePath); - } + // If PKCS#12 fails, try PEM format + clientCertificate = X509Certificate2.CreateFromPemFile(clientCertificatePath); } if (!clientCertificate.HasPrivateKey) @@ -148,22 +118,9 @@ public static X509Certificate2 LoadClientCertificate( try { - X509Certificate2 clientCertificate; - - // Choose the appropriate method based on whether a password is provided - if (!string.IsNullOrEmpty(clientKeyPassword)) - { - clientCertificate = X509Certificate2.CreateFromEncryptedPemFile( - clientCertificatePath, - clientKeyPath, - clientKeyPassword); - } - else - { - clientCertificate = X509Certificate2.CreateFromPemFile( - clientCertificatePath, - clientKeyPath); - } + X509Certificate2 clientCertificate = X509Certificate2.CreateFromPemFile( + clientCertificatePath, + clientKeyPath); if (!clientCertificate.HasPrivateKey) { @@ -198,21 +155,6 @@ public static X509Certificate2 LoadClientCertificate( public static bool ValidateCertificateChain( X509Certificate2 certificate, string certificateType) - { - return ValidateCertificateChain(certificate, certificateType, null); - } - - /// - /// Validates the certificate chain for a given certificate with optional configuration. - /// - /// The certificate to validate. - /// Type description for logging (e.g., "Client certificate"). - /// Optional configuration to read environment variables from. - /// True if the certificate chain is valid; otherwise, false. - public static bool ValidateCertificateChain( - X509Certificate2 certificate, - string certificateType, - IConfiguration? configuration) { try { @@ -220,14 +162,8 @@ public static bool ValidateCertificateChain( // Configure chain policy chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; - - // Configure RevocationMode from environment variable or use default - var revocationMode = GetRevocationModeFromConfiguration(configuration); - chain.ChainPolicy.RevocationMode = revocationMode; - - // Configure RevocationFlag from environment variable or use default - var revocationFlag = GetRevocationFlagFromConfiguration(configuration); - chain.ChainPolicy.RevocationFlag = revocationFlag; + chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; bool isValid = chain.Build(certificate); @@ -356,57 +292,6 @@ private static void ValidateFileExists(string filePath, string fileType) throw new FileNotFoundException($"{fileType} file not found at path: {filePath}", filePath); } } - - /// - /// Gets the X509RevocationMode from configuration or returns the default value. - /// - /// Configuration to read from. - /// The configured revocation mode or default (Online). - private static X509RevocationMode GetRevocationModeFromConfiguration(IConfiguration? configuration) - { - if (configuration == null) - { - return X509RevocationMode.Online; - } - - if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, out var modeString)) - { - if (Enum.TryParse(modeString, true, out var mode)) - { - return mode; - } - - ((IConfigurationExtensionsLogger)OpenTelemetryProtocolExporterEventSource.Log).LogInvalidConfigurationValue( - OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, - modeString); - } - - return X509RevocationMode.Online; - } - - /// - /// Gets the X509RevocationFlag from configuration or returns the default value. - /// - /// Configuration to read from. - /// The configured revocation flag or default (ExcludeRoot). - private static X509RevocationFlag GetRevocationFlagFromConfiguration(IConfiguration? configuration) - { - if (configuration != null && configuration.TryGetStringValue(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, out var flagString)) - { - if (Enum.TryParse(flagString, true, out var flag)) - { - return flag; - } - - ((IConfigurationExtensionsLogger)OpenTelemetryProtocolExporterEventSource.Log).LogInvalidConfigurationValue( - OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, - flagString); - } - - // Use ExcludeRoot as default to avoid revocation checks on the root CA certificate, - // which is typically self-signed and may not have revocation information available - return X509RevocationFlag.ExcludeRoot; - } } #endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs index 6710522661f..a0f5169b0aa 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpMtlsHttpClientFactory.cs @@ -58,15 +58,13 @@ public static HttpClient CreateMtlsHttpClient( // Load certificate without separate key file (e.g., PKCS#12 format) clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate( mtlsOptions.ClientCertificatePath, - null, - mtlsOptions.ClientKeyPassword); + null); } else { clientCertificate = OtlpMtlsCertificateManager.LoadClientCertificate( mtlsOptions.ClientCertificatePath, - mtlsOptions.ClientKeyPath, - mtlsOptions.ClientKeyPassword); + mtlsOptions.ClientKeyPath); } if (mtlsOptions.EnableCertificateChainValidation) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs index 02957c61825..8e8fd497b0d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs @@ -36,9 +36,4 @@ internal static class OtlpSpecConfigDefinitions 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"; - public const string ClientKeyPasswordEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD"; - - // Certificate validation environment variables - public const string CertificateRevocationModeEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE"; - public const string CertificateRevocationFlagEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG"; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 86c6a702d63..b3f67444bf0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -333,13 +333,6 @@ private void ApplyMtlsConfiguration(IConfiguration configuration) this.MtlsOptions ??= new(); this.MtlsOptions.ClientKeyPath = clientKeyPath; } - - // Check and apply client key password from environment variable - if (configuration.TryGetStringValue(OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName, out var clientKeyPassword)) - { - this.MtlsOptions ??= new(); - this.MtlsOptions.ClientKeyPassword = clientKeyPassword; - } } #endif } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs index 763cb43a8a0..dc2feaa8e8e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMtlsOptions.cs @@ -22,13 +22,6 @@ internal sealed class OtlpMtlsOptions /// public string? ClientKeyPath { get; set; } - /// - /// Gets or sets the password for the client private key file when it is encrypted. - /// This is only used when the private key file is password-protected. - /// If not provided and the key file is encrypted, certificate loading will fail. - /// - public string? ClientKeyPassword { 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. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 85b104899e1..b358a7b9edc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -458,9 +458,6 @@ or reader | `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) | - | `OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD` | `ClientKeyPassword` | Password for encrypted client private key file (PEM) | - | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE` | N/A | Certificate revocation mode (`Online`, `Offline`, or `NoCheck`). Default: `Online` | - | `OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG` | N/A | Certificate revocation flag (`ExcludeRoot`, `EntireChain`, or `EndCertificateOnly`). Default: `ExcludeRoot` | * Logs: diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 8b03b3cd92a..52271cf5b34 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -342,32 +342,6 @@ public void OtlpExporterOptions_MtlsEnvironmentVariables_NoEnvironmentVariables( Assert.Null(options.MtlsOptions); } - [Fact] - public void OtlpExporterOptions_MtlsEnvironmentVariables_ClientKeyPassword() - { - // Test client key password environment variable - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", "/path/to/client.crt"); - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", "/path/to/client.key"); - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD", "secret123"); - - 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.Equal("secret123", options.MtlsOptions.ClientKeyPassword); - Assert.True(options.MtlsOptions.IsEnabled); - } - finally - { - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE", null); - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY", null); - Environment.SetEnvironmentVariable("OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD", null); - } - } - [Fact] public void OtlpExporterOptions_MtlsEnvironmentVariables_UsingIConfiguration() { diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs index e15ef1628fc..d3fac9900a7 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsCertificateManagerTests.cs @@ -3,7 +3,6 @@ #if NET -using Microsoft.Extensions.Configuration; using Xunit; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; @@ -151,166 +150,7 @@ public void ValidateCertificateChain_ReturnsResult_WithValidCertificate() } [Fact] - public void ValidateCertificateChain_UsesDefaultConfiguration_WhenConfigurationIsNull() - { - using var cert = CreateSelfSignedCertificate(); - - // Both overloads should work - var result1 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate"); - var result2 = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", null); - - // Results should be the same since both use defaults - Assert.Equal(result1, result2); - } - - [Fact] - public void ValidateCertificateChain_UsesRevocationModeFromConfiguration() - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "NoCheck"), - }) - .Build(); - - // Should not throw when using NoCheck mode - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing - Assert.True(result || !result); - } - - [Fact] - public void ValidateCertificateChain_UsesRevocationFlagFromConfiguration() - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "EntireChain"), - }) - .Build(); - - // Should not throw when using EntireChain flag - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing - Assert.True(result || !result); - } - - [Fact] - public void ValidateCertificateChain_UsesBothRevocationConfigurationValues() - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "Offline"), - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "EndCertificateOnly"), - }) - .Build(); - - // Should not throw when using both configuration values - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing - Assert.True(result || !result); - } - - [Fact] - public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationMode() - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, "InvalidMode"), - }) - .Build(); - - // Should not throw even with invalid configuration value (should use default) - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing and use default Online mode - Assert.True(result || !result); - } - - [Fact] - public void ValidateCertificateChain_UsesDefaultsForInvalidRevocationFlag() - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, "InvalidFlag"), - }) - .Build(); - - // Should not throw even with invalid configuration value (should use default) - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing and use default ExcludeRoot flag - Assert.True(result || !result); - } - - [Theory] - [InlineData("Online")] - [InlineData("Offline")] - [InlineData("NoCheck")] - [InlineData("online")] - [InlineData("OFFLINE")] - [InlineData("nocheck")] - public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationMode(string revocationMode) - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, revocationMode), - }) - .Build(); - - // Should handle case-insensitive enum parsing - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing - Assert.True(result || !result); - } - - [Theory] - [InlineData("ExcludeRoot")] - [InlineData("EntireChain")] - [InlineData("EndCertificateOnly")] - [InlineData("excluderoot")] - [InlineData("ENTIRECHAIN")] - [InlineData("endcertificateonly")] - public void ValidateCertificateChain_HandlesCaseInsensitiveRevocationFlag(string revocationFlag) - { - using var cert = CreateSelfSignedCertificate(); - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair(OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, revocationFlag), - }) - .Build(); - - // Should handle case-insensitive enum parsing - var result = OpenTelemetryProtocol.Implementation.OtlpMtlsCertificateManager.ValidateCertificateChain(cert, "test certificate", configuration); - - // The method should execute without throwing - Assert.True(result || !result); - } - - [Fact] - public void LoadClientCertificate_AcceptsPasswordParameter() + public void LoadClientCertificate_LoadsFromSeparateFiles() { var tempCertFile = Path.GetTempFileName(); var tempKeyFile = Path.GetTempFileName(); @@ -319,14 +159,13 @@ public void LoadClientCertificate_AcceptsPasswordParameter() try { - // This test verifies that the method accepts the password parameter + // 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, - "test-password")); + tempKeyFile)); // The exception should be about certificate loading, not method signature Assert.Contains("Failed to load client certificate", exception.Message, StringComparison.OrdinalIgnoreCase); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs index 689e183f630..89fdfed9841 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsHttpClientFactoryTests.cs @@ -39,13 +39,12 @@ public void CreateHttpClient_ConfiguresClientCertificate_WhenValidCertificatePro { // Create a self-signed certificate for testing using var cert = CreateSelfSignedCertificate(); - var certBytes = cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx, "testpassword"); + var certBytes = cert.Export(System.Security.Cryptography.X509Certificates.X509ContentType.Pfx); File.WriteAllBytes(tempCertFile, certBytes); var options = new OtlpMtlsOptions { ClientCertificatePath = tempCertFile, - ClientKeyPassword = "testpassword", EnableCertificateChainValidation = false, // Ignore validation for test cert }; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs index b96dabb3a9a..e5d9128fe4b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMtlsOptionsTests.cs @@ -28,14 +28,12 @@ public void Properties_CanBeSet() { ClientCertificatePath = "/path/to/client.crt", ClientKeyPath = "/path/to/client.key", - ClientKeyPassword = "secret123", 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("secret123", options.ClientKeyPassword); Assert.Equal("/path/to/ca.crt", options.CaCertificatePath); Assert.False(options.EnableCertificateChainValidation); Assert.True(options.IsEnabled); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs index 52ebf4b621e..11c9b5b33d0 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpSpecConfigDefinitionsTests.cs @@ -9,24 +9,6 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public class OtlpSpecConfigDefinitionsTests { - [Fact] - public void CertificateRevocationModeEnvVarName_HasCorrectValue() - { - Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_MODE", OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName); - } - - [Fact] - public void CertificateRevocationFlagEnvVarName_HasCorrectValue() - { - Assert.Equal("OTEL_EXPORTER_OTLP_CERTIFICATE_REVOCATION_FLAG", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName); - } - - [Fact] - public void ClientKeyPasswordEnvVarName_HasCorrectValue() - { - Assert.Equal("OTEL_EXPORTER_OTLP_CLIENT_KEY_PASSWORD", OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName); - } - [Fact] public void AllEnvironmentVariableNames_AreUnique() { @@ -39,23 +21,12 @@ public void AllEnvironmentVariableNames_AreUnique() OtlpSpecConfigDefinitions.CertificateEnvVarName, OtlpSpecConfigDefinitions.ClientKeyEnvVarName, OtlpSpecConfigDefinitions.ClientCertificateEnvVarName, - OtlpSpecConfigDefinitions.ClientKeyPasswordEnvVarName, - OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, - OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, }; var uniqueVars = envVars.Distinct().ToArray(); Assert.Equal(envVars.Length, uniqueVars.Length); } - - [Fact] - public void CertificateRevocationEnvironmentVariables_FollowNamingConvention() - { - // All certificate-related environment variables should follow the OTEL_EXPORTER_OTLP_CERTIFICATE prefix - Assert.StartsWith("OTEL_EXPORTER_OTLP_CERTIFICATE", OtlpSpecConfigDefinitions.CertificateRevocationModeEnvVarName, StringComparison.Ordinal); - Assert.StartsWith("OTEL_EXPORTER_OTLP_CERTIFICATE", OtlpSpecConfigDefinitions.CertificateRevocationFlagEnvVarName, StringComparison.Ordinal); - } } #endif