diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..a8e346f06de 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +OpenTelemetry.Exporter.OtlpExporterOptions.CertificateFile.get -> string! +OpenTelemetry.Exporter.OtlpExporterOptions.CertificateFile.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.ClientCertificateFile.get -> string! +OpenTelemetry.Exporter.OtlpExporterOptions.ClientCertificateFile.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.ClientKeyFile.get -> string! +OpenTelemetry.Exporter.OtlpExporterOptions.ClientKeyFile.set -> void diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 355ab376743..091785dab76 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -11,6 +11,9 @@ using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Internal; using OpenTelemetry.Trace; +#if NET6_0_OR_GREATER +using System.Security.Cryptography.X509Certificates; +#endif namespace OpenTelemetry.Exporter; @@ -28,6 +31,10 @@ public class OtlpExporterOptions : IOtlpExporterOptions internal const string DefaultHttpEndpoint = "http://localhost:4318"; internal const OtlpExportProtocol DefaultOtlpExportProtocol = OtlpExportProtocol.Grpc; + internal const string CertificateFileEnvVarName = "OTEL_EXPORTER_OTLP_CERTIFICATE"; + internal const string ClientKeyFileEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_KEY"; + internal const string ClientCertificateFileEnvVarName = "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"; + internal static readonly KeyValuePair[] StandardHeaders = new KeyValuePair[] { new("User-Agent", GetUserAgentString()), @@ -68,13 +75,26 @@ internal OtlpExporterOptions( this.DefaultHttpClientFactory = () => { +#if NET6_0_OR_GREATER + var handler = new HttpClientHandler(); + HttpClient client = this.AddCertificatesToHttpClient(handler); + client.Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds); + return client; +#else + // For earlier .NET versions return new HttpClient { Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), }; +#endif }; this.BatchExportProcessorOptions = defaultBatchOptions!; + + // Load certificate-related environment variables + this.CertificateFile = Environment.GetEnvironmentVariable(CertificateFileEnvVarName) ?? string.Empty; + this.ClientKeyFile = Environment.GetEnvironmentVariable(ClientKeyFileEnvVarName) ?? string.Empty; + this.ClientCertificateFile = Environment.GetEnvironmentVariable(ClientCertificateFileEnvVarName) ?? string.Empty; } /// @@ -142,6 +162,21 @@ public Func HttpClientFactory } } + /// + /// Gets or sets the trusted certificate to use when verifying a server's TLS credentials. + /// + public string CertificateFile { get; set; } + + /// + /// Gets or sets the path to the private key to use in mTLS communication in PEM format. + /// + public string ClientKeyFile { get; set; } + + /// + /// Gets or sets the path to the certificate/chain trust for client's private key to use in mTLS communication in PEM format. + /// + public string ClientCertificateFile { get; set; } + /// /// Gets a value indicating whether or not the signal-specific path should /// be appended to . @@ -220,6 +255,41 @@ internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOp return this; } +#if NET6_0_OR_GREATER + internal HttpClient AddCertificatesToHttpClient(HttpClientHandler handler) + { + // Configure server certificate validation if CertificateFile is provided + if (!string.IsNullOrEmpty(this.CertificateFile)) + { + // Load the certificate from the file + var trustedCertificate = X509Certificate2.CreateFromPemFile(this.CertificateFile); + + // Set custom server certificate validation callback + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => + { + if (cert != null && chain != null) + { + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(trustedCertificate); + return chain.Build(cert); + } + + return false; + }; + } + + // Add client certificate if both files are provided + if (!string.IsNullOrEmpty(this.ClientCertificateFile) && !string.IsNullOrEmpty(this.ClientKeyFile)) + { + var clientCertificate = X509Certificate2.CreateFromPemFile(this.ClientCertificateFile, this.ClientKeyFile); + handler.ClientCertificates.Add(clientCertificate); + } + + // Create and return an HttpClient with the modified handler + return new HttpClient(handler); + } +#endif + private static string GetUserAgentString() { var assembly = typeof(OtlpExporterOptions).Assembly; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs index c4bea6931e3..437175e6a7a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -36,7 +36,30 @@ public static Channel CreateChannel(this OtlpExporterOptions options) ChannelCredentials channelCredentials; if (options.Endpoint.Scheme == Uri.UriSchemeHttps) { - channelCredentials = new SslCredentials(); + if (!string.IsNullOrEmpty(options.ClientCertificateFile) && !string.IsNullOrEmpty(options.ClientKeyFile)) + { + string clientCertPem = File.ReadAllText(options.ClientCertificateFile); + string clientKeyPem = File.ReadAllText(options.ClientKeyFile); + var keyPair = new KeyCertificatePair(clientCertPem, clientKeyPem); + + string rootCertPem = string.Empty; + if (!string.IsNullOrEmpty(options.CertificateFile)) + { + rootCertPem = File.ReadAllText(options.CertificateFile); + } + + channelCredentials = new SslCredentials(rootCertPem, keyPair); + } + else + { + string rootCertPem = string.Empty; + if (!string.IsNullOrEmpty(options.CertificateFile)) + { + rootCertPem = File.ReadAllText(options.CertificateFile); + } + + channelCredentials = new SslCredentials(rootCertPem); + } } else {