From ad74a08497594c47f36e1515ecc9c46fe7b3be90 Mon Sep 17 00:00:00 2001 From: krabhishek8260 Date: Sat, 17 Jun 2017 14:11:52 -0700 Subject: [PATCH] Add support for connecting/authenticating through kubeconfig --- .gitignore | 6 + README.md | 1 + examples/simple/PodList.cs | 19 +- examples/simple/simple.csproj | 28 +-- src/Exceptions/KubeConfigException.cs | 24 ++ src/Exceptions/KubernetesClientException.cs | 24 ++ src/KubeConfigModels/Cluster.cs | 13 ++ src/KubeConfigModels/ClusterEndpoint.cs | 16 ++ src/KubeConfigModels/Context.cs | 13 ++ src/KubeConfigModels/ContextDetails.cs | 17 ++ src/KubeConfigModels/K8SConfiguration.cs | 29 +++ src/KubeConfigModels/User.cs | 14 ++ src/KubeConfigModels/UserCrednetials.cs | 23 ++ src/Kubernetes.Auth.cs | 125 ++++++++++ ...{csharp.csproj => KubernetesClient.csproj} | 5 + src/KubernetesClientConfiguration.cs | 220 ++++++++++++++++++ src/KubernetesClientCredentials.cs | 70 ++++++ src/Utils.cs | 86 +++++++ 18 files changed, 709 insertions(+), 24 deletions(-) create mode 100644 src/Exceptions/KubeConfigException.cs create mode 100644 src/Exceptions/KubernetesClientException.cs create mode 100644 src/KubeConfigModels/Cluster.cs create mode 100644 src/KubeConfigModels/ClusterEndpoint.cs create mode 100644 src/KubeConfigModels/Context.cs create mode 100644 src/KubeConfigModels/ContextDetails.cs create mode 100644 src/KubeConfigModels/K8SConfiguration.cs create mode 100644 src/KubeConfigModels/User.cs create mode 100644 src/KubeConfigModels/UserCrednetials.cs create mode 100644 src/Kubernetes.Auth.cs rename src/{csharp.csproj => KubernetesClient.csproj} (59%) mode change 100755 => 100644 create mode 100644 src/KubernetesClientConfiguration.cs create mode 100644 src/KubernetesClientCredentials.cs create mode 100644 src/Utils.cs diff --git a/.gitignore b/.gitignore index 800d87b..201f857 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ .vs obj/ bin/ + +# User-specific VS files +*.suo +*.user +*.userosscache +*.sln.docstates diff --git a/README.md b/README.md index 9f2fbd6..40404b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # csharp Work In Progress +Currently only supported on Linux # Generating the client code diff --git a/examples/simple/PodList.cs b/examples/simple/PodList.cs index 9fa43a0..f65f8d1 100755 --- a/examples/simple/PodList.cs +++ b/examples/simple/PodList.cs @@ -1,18 +1,17 @@ -using System; - -using k8s; - -namespace simple +namespace simple { + using System; + using System.IO; + using k8s; + class PodList { static void Main(string[] args) { - IKubernetes client = new Kubernetes(); - client.BaseUri = new Uri("http://localhost:8001"); - var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default"); - listTask.Wait(); - var list = listTask.Result.Body; + var k8sClientConfig = new KubernetesClientConfiguration(); + IKubernetes client = new Kubernetes(k8sClientConfig); + var listTask = client.ListNamespacedPodWithHttpMessagesAsync("default").Result; + var list = listTask.Body; foreach (var item in list.Items) { Console.WriteLine(item.Metadata.Name); } diff --git a/examples/simple/simple.csproj b/examples/simple/simple.csproj index e16c63b..b965b51 100755 --- a/examples/simple/simple.csproj +++ b/examples/simple/simple.csproj @@ -1,14 +1,14 @@ - - - - - - - - - - Exe - netcoreapp1.1 - - - + + + + + + + + + + Exe + netcoreapp1.1 + + + diff --git a/src/Exceptions/KubeConfigException.cs b/src/Exceptions/KubeConfigException.cs new file mode 100644 index 0000000..534434a --- /dev/null +++ b/src/Exceptions/KubeConfigException.cs @@ -0,0 +1,24 @@ +namespace k8s.Exceptions +{ + using System; + + /// + /// The exception that is thrown when the kube config is invalid + /// + public class KubeConfigException : Exception + { + public KubeConfigException() + { + } + + public KubeConfigException(string message) + : base(message) + { + } + + public KubeConfigException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/Exceptions/KubernetesClientException.cs b/src/Exceptions/KubernetesClientException.cs new file mode 100644 index 0000000..765dfda --- /dev/null +++ b/src/Exceptions/KubernetesClientException.cs @@ -0,0 +1,24 @@ +namespace k8s.Exceptions +{ + using System; + + /// + /// The exception that is thrown when there is a client exception + /// + public class KubernetesClientException : Exception + { + public KubernetesClientException() + { + } + + public KubernetesClientException(string message) + : base(message) + { + } + + public KubernetesClientException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/KubeConfigModels/Cluster.cs b/src/KubeConfigModels/Cluster.cs new file mode 100644 index 0000000..9cd6d56 --- /dev/null +++ b/src/KubeConfigModels/Cluster.cs @@ -0,0 +1,13 @@ +namespace k8s.KubeConfigModels +{ + using YamlDotNet.Serialization; + + public class Cluster + { + [YamlMember(Alias = "cluster")] + public ClusterEndpoint ClusterEndpoint { get; set; } + + [YamlMember(Alias = "name")] + public string Name { get; set; } + } +} diff --git a/src/KubeConfigModels/ClusterEndpoint.cs b/src/KubeConfigModels/ClusterEndpoint.cs new file mode 100644 index 0000000..fef1786 --- /dev/null +++ b/src/KubeConfigModels/ClusterEndpoint.cs @@ -0,0 +1,16 @@ +namespace k8s.KubeConfigModels +{ + using YamlDotNet.Serialization; + + public class ClusterEndpoint + { + [YamlMember(Alias = "certificate-authority-data")] + public string CertificateAuthorityData { get; set; } + + [YamlMember(Alias = "server")] + public string Server { get; set; } + + [YamlMember(Alias = "insecure-skip-tls-verify")] + public bool SkipTlsVerify { get; set; } + } +} diff --git a/src/KubeConfigModels/Context.cs b/src/KubeConfigModels/Context.cs new file mode 100644 index 0000000..73db6ba --- /dev/null +++ b/src/KubeConfigModels/Context.cs @@ -0,0 +1,13 @@ +namespace k8s.KubeConfigModels +{ + using YamlDotNet.Serialization; + + public class Context + { + [YamlMember(Alias = "context")] + public ContextDetails ContextDetails { get; set; } + + [YamlMember(Alias = "name")] + public string Name { get; set; } + } +} diff --git a/src/KubeConfigModels/ContextDetails.cs b/src/KubeConfigModels/ContextDetails.cs new file mode 100644 index 0000000..291f587 --- /dev/null +++ b/src/KubeConfigModels/ContextDetails.cs @@ -0,0 +1,17 @@ +namespace k8s.KubeConfigModels +{ + using YamlDotNet.RepresentationModel; + using YamlDotNet.Serialization; + + public class ContextDetails + { + [YamlMember(Alias = "cluster")] + public string Cluster { get; set; } + + [YamlMember(Alias = "user")] + public string User { get; set; } + + [YamlMember(Alias = "namespace")] + public string Namespace { get; set; } + } +} diff --git a/src/KubeConfigModels/K8SConfiguration.cs b/src/KubeConfigModels/K8SConfiguration.cs new file mode 100644 index 0000000..7c49098 --- /dev/null +++ b/src/KubeConfigModels/K8SConfiguration.cs @@ -0,0 +1,29 @@ +namespace k8s.KubeConfigModels +{ + using System.Collections.Generic; + using YamlDotNet.Serialization; + + /// + /// kubeconfig configuration model + /// + public class K8SConfiguration + { + [YamlMember(Alias = "apiVersion")] + public string ApiVersion { get; set; } + + [YamlMember(Alias = "kind")] + public string Kind { get; set; } + + [YamlMember(Alias = "current-context")] + public string CurrentContext { get; set; } + + [YamlMember(Alias = "contexts")] + public IEnumerable Contexts { get; set; } + + [YamlMember(Alias = "clusters")] + public IEnumerable Clusters { get; set; } + + [YamlMember(Alias = "users")] + public IEnumerable Users { get; set; } + } +} diff --git a/src/KubeConfigModels/User.cs b/src/KubeConfigModels/User.cs new file mode 100644 index 0000000..2a3baa1 --- /dev/null +++ b/src/KubeConfigModels/User.cs @@ -0,0 +1,14 @@ +namespace k8s.KubeConfigModels +{ + using YamlDotNet.RepresentationModel; + using YamlDotNet.Serialization; + + public class User + { + [YamlMember(Alias = "user")] + public UserCrednetials UserCredentials { get; set; } + + [YamlMember(Alias = "name")] + public string Name { get; set; } + } +} diff --git a/src/KubeConfigModels/UserCrednetials.cs b/src/KubeConfigModels/UserCrednetials.cs new file mode 100644 index 0000000..2078814 --- /dev/null +++ b/src/KubeConfigModels/UserCrednetials.cs @@ -0,0 +1,23 @@ +namespace k8s.KubeConfigModels +{ + using YamlDotNet.RepresentationModel; + using YamlDotNet.Serialization; + + public class UserCrednetials + { + [YamlMember(Alias = "client-certificate-data")] + public string ClientCertificateData { get; set; } + + [YamlMember(Alias = "client-key-data")] + public string ClientKeyData { get; set; } + + [YamlMember(Alias = "token")] + public string Token { get; set; } + + [YamlMember(Alias = "userName")] + public string UserName { get; set; } + + [YamlMember(Alias = "password")] + public string Password { get; set; } + } +} diff --git a/src/Kubernetes.Auth.cs b/src/Kubernetes.Auth.cs new file mode 100644 index 0000000..d372228 --- /dev/null +++ b/src/Kubernetes.Auth.cs @@ -0,0 +1,125 @@ +namespace k8s +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Net.Http; + using System.Net.Security; + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using k8s.Exceptions; + using Microsoft.Rest; + + public partial class Kubernetes : ServiceClient, IKubernetes + { + /// + /// Initializes a new instance of the class. + /// + /// + /// Optional. The delegating handlers to add to the http client pipeline. + /// + public Kubernetes(KubernetesClientConfiguration config) + { + this.Initialize(); + + this.CaCert = Utils.Base64Decode(config.SslCaCert); + this.BaseUri = new Uri(config.Host); + + // ssl cert validation + Func sslCertValidationFunc; + if (config.SkipTlsVerify) + { + sslCertValidationFunc = (sender, certificate, chain, sslPolicyErrors) => true; + } + else + { + sslCertValidationFunc = this.CertificateValidationCallBack; + } + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = sslCertValidationFunc + }; + + // set credentails for the kubernernet client + this.SetCredentialsAsync(config, handler).Wait(); + this.InitializeHttpClient(handler); + } + + private string CaCert { get; set; } + + /// + /// Set credentials for the Client + /// + /// k8s client configuration + /// http client handler for the rest client + /// Task + private async Task SetCredentialsAsync(KubernetesClientConfiguration config, HttpClientHandler handler) + { + // set the Credentails for token based auth + if (!string.IsNullOrWhiteSpace(config.AccessToken)) + { + this.Credentials = new KubernetesClientCredentials(config.AccessToken); + } + else if (!string.IsNullOrWhiteSpace(config.Username) && !string.IsNullOrWhiteSpace(config.Password)) + { + this.Credentials = new KubernetesClientCredentials(config.Username, config.Password); + } + // othwerwise set handler for clinet cert based auth + else if (!string.IsNullOrWhiteSpace(config.ClientCertificateData) && !string.IsNullOrWhiteSpace(config.ClientCertificateKey)) + { + var pfxFilePath = await Utils.GeneratePfxAsync(config).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(pfxFilePath)) + { + throw new KubernetesClientException("Failed to generate pfx file"); + } + + var cert = new X509Certificate2(pfxFilePath, string.Empty, X509KeyStorageFlags.PersistKeySet); + handler.ClientCertificates.Add(cert); + } + else + { + throw new KubeConfigException("Configuration does not have appropriate auth credentials"); + } + } + + /// + /// SSl Cert Validation Callback + /// + /// sender + /// client certificate + /// chain + /// ssl policy errors + /// true if valid cert + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Unused by design")] + private bool CertificateValidationCallBack( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + // If the certificate is a valid, signed certificate, return true. + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + // If there are errors in the certificate chain, look at each error to determine the cause. + if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0) + { + X509Chain chain0 = new X509Chain(); + chain0.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + // add all your extra certificate chain + chain0.ChainPolicy.ExtraStore.Add(new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(this.CaCert))); + chain0.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + var isValid = chain0.Build((X509Certificate2)certificate); + return isValid; + } + else + { + // In all other cases, return false. + return false; + } + } + } +} diff --git a/src/csharp.csproj b/src/KubernetesClient.csproj old mode 100755 new mode 100644 similarity index 59% rename from src/csharp.csproj rename to src/KubernetesClient.csproj index 10f59b9..6cf6d0a --- a/src/csharp.csproj +++ b/src/KubernetesClient.csproj @@ -3,8 +3,13 @@ Library netcoreapp1.1 + + + + + \ No newline at end of file diff --git a/src/KubernetesClientConfiguration.cs b/src/KubernetesClientConfiguration.cs new file mode 100644 index 0000000..33e3e5b --- /dev/null +++ b/src/KubernetesClientConfiguration.cs @@ -0,0 +1,220 @@ +namespace k8s +{ + using System; + using System.IO; + using System.Linq; + using k8s.Exceptions; + using k8s.KubeConfigModels; + using YamlDotNet.Serialization; + + /// + /// Represents a set of kubernetes client configuration settings + /// + public class KubernetesClientConfiguration + { + /// + /// Initializes a new instance of the class. + /// Initializes a new instance of the ClientConfiguration class + /// + /// kubeconfig file info + /// Context to use from kube config + public KubernetesClientConfiguration(FileInfo kubeconfig = null, string currentContext = null) + { + if (kubeconfig == null) + { + kubeconfig = new FileInfo(KubeConfigDefaultLocation); + } + var k8SConfig = this.LoadKubeConfig(kubeconfig); + this.Initialize(k8SConfig, currentContext); + } + + /// + /// kubeconfig Default Location + /// + private static readonly string KubeConfigDefaultLocation = Path.Combine(Environment.GetEnvironmentVariable("HOME"), ".kube/config"); + + /// + /// Gets CurrentContext + /// + public string CurrentContext { get; private set; } + + /// + /// Gets Host + /// + public string Host { get; private set; } + + /// + /// Gets SslCaCert + /// + public string SslCaCert { get; private set; } + + /// + /// Gets ClientCertificateData + /// + public string ClientCertificateData { get; private set; } + + /// + /// Gets ClientCertificate Key + /// + public string ClientCertificateKey { get; private set; } + + /// + /// Gets a value indicating whether to skip ssl server cert validation + /// + public bool SkipTlsVerify { get; private set; } + + /// + /// Gets or sets the HTTP user agent. + /// + /// Http user agent. + public string UserAgent { get; set; } + + /// + /// Gets or sets the username (HTTP basic authentication). + /// + /// The username. + public string Username { get; set; } + + /// + /// Gets or sets the password (HTTP basic authentication). + /// + /// The password. + public string Password { get; set; } + + /// + /// Gets or sets the access token for OAuth2 authentication. + /// + /// The access token. + public string AccessToken { get; set; } + + /// + /// Validates and Intializes Client Configuration + /// + /// Kubernetes Configuration + /// Current Context + private void Initialize(K8SConfiguration k8SConfig, string currentContext = null) + { + Context activeContext; + + // set the currentCOntext to passed context if not null + if (!string.IsNullOrWhiteSpace(currentContext)) + { + if (k8SConfig.Contexts == null) + { + throw new KubeConfigException("No contexts found in kubeconfig"); + } + + activeContext = k8SConfig.Contexts.FirstOrDefault(c => c.Name.Equals(currentContext, StringComparison.OrdinalIgnoreCase)); + if (activeContext != null) + { + this.CurrentContext = activeContext.Name; + } + else + { + throw new KubeConfigException($"CurrentContext: {0} not found in contexts in kubeconfig"); + } + } + // otherwise set current context from kubeconfig + else + { + activeContext = k8SConfig.Contexts.FirstOrDefault(c => c.Name.Equals(k8SConfig.CurrentContext, StringComparison.OrdinalIgnoreCase)); + + if (activeContext == null) + { + throw new KubeConfigException($"CurrentContext: {currentContext} not found in contexts in kubeconfig"); + } + + this.CurrentContext = activeContext.Name; + } + + var clusterDetails = k8SConfig.Clusters.FirstOrDefault(c => c.Name.Equals(activeContext.ContextDetails.Cluster, StringComparison.OrdinalIgnoreCase)); + if (clusterDetails?.ClusterEndpoint != null) + { + if (string.IsNullOrWhiteSpace(clusterDetails.ClusterEndpoint.Server)) + { + throw new KubeConfigException($"server not found for current-context :{activeContext} in kubeconfig"); + } + + if (!clusterDetails.ClusterEndpoint.SkipTlsVerify && + string.IsNullOrWhiteSpace(clusterDetails.ClusterEndpoint.CertificateAuthorityData)) + { + throw new KubeConfigException($"certificate-authority-data not found for current-context :{activeContext} in kubeconfig"); + } + + this.Host = clusterDetails.ClusterEndpoint.Server; + this.SslCaCert = clusterDetails.ClusterEndpoint.CertificateAuthorityData; + this.SkipTlsVerify = clusterDetails.ClusterEndpoint.SkipTlsVerify; + } + else + { + throw new KubeConfigException($"Cluster details not found for current-context: {activeContext} in kubeconfig"); + } + + // set user details from kubeconfig + var userDetails = k8SConfig.Users.FirstOrDefault(c => c.Name.Equals(activeContext.ContextDetails.User, StringComparison.OrdinalIgnoreCase)); + + this.SetUserDetails(userDetails); + } + + private void SetUserDetails(User userDetails) + { + if (userDetails == null) + { + throw new KubeConfigException("User not found for the current context in kubeconfig"); + } + + if (userDetails.UserCredentials == null) + { + throw new KubeConfigException($"User credentials not found for user: {userDetails.Name} in kubeconfig"); + } + + var userCredentialsFound = false; + + // Basic and bearer tokens are mutually exclusive + if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.Token)) + { + this.AccessToken = userDetails.UserCredentials.Token; + userCredentialsFound = true; + } + else if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.UserName) && + !string.IsNullOrWhiteSpace(userDetails.UserCredentials.Password)) + { + this.Username = userDetails.UserCredentials.UserName; + this.Password = userDetails.UserCredentials.Password; + userCredentialsFound = true; + } + + // Token and cert based auth can co-exist + if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificateData) && + !string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKeyData)) + { + this.ClientCertificateData = userDetails.UserCredentials.ClientCertificateData; + this.ClientCertificateKey = userDetails.UserCredentials.ClientKeyData; + userCredentialsFound = true; + } + + if (!userCredentialsFound) + { + throw new KubeConfigException($"User: {userDetails.Name} does not have appropriate auth credentials in kube config"); + } + } + + /// + /// Loads Kube Config + /// + /// Kube config file contents + /// Instance of the class + private K8SConfiguration LoadKubeConfig(FileInfo kubeconfig) + { + if (!kubeconfig.Exists) + { + throw new KubeConfigException($"kubeconfig file not found at {kubeconfig.FullName}"); + } + var kubeconfigContent = File.ReadAllText(kubeconfig.FullName); + + var deserializeBuilder = new DeserializerBuilder(); + var deserializer = deserializeBuilder.Build(); + return deserializer.Deserialize(kubeconfigContent); + } + } +} diff --git a/src/KubernetesClientCredentials.cs b/src/KubernetesClientCredentials.cs new file mode 100644 index 0000000..d67b447 --- /dev/null +++ b/src/KubernetesClientCredentials.cs @@ -0,0 +1,70 @@ +namespace k8s +{ + using System; + using System.Globalization; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading; + using System.Threading.Tasks; + using k8s.Exceptions; + using Microsoft.Rest; + + /// + /// Class to set the Kubernetes Client Credentials for token based auth + /// + public class KubernetesClientCredentials : ServiceClientCredentials + { + public KubernetesClientCredentials(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + this.AuthenticationToken = token; + this.AuthenticationScheme = "Bearer"; + } + + public KubernetesClientCredentials(string userName, string password) + { + if (string.IsNullOrWhiteSpace(userName)) + { + throw new ArgumentNullException(nameof(userName)); + } + + if (string.IsNullOrWhiteSpace(userName)) + { + throw new ArgumentNullException(nameof(password)); + } + + this.AuthenticationToken = Utils.Base64Encode(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", userName, password)); + this.AuthenticationScheme = "Basic"; + } + + private string AuthenticationToken { get; } + + private string AuthenticationScheme { get; } + + public override async Task ProcessHttpRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrWhiteSpace(this.AuthenticationScheme)) + { + throw new KubernetesClientException("AuthenticationScheme cannot be null. Please set the AuthenticationScheme to Basic/Bearer"); + } + + if (string.IsNullOrWhiteSpace(this.AuthenticationToken)) + { + throw new KubernetesClientException("AuthenticationToken cannot be null. Please set the authentication token"); + } + + request.Headers.Authorization = new AuthenticationHeaderValue(this.AuthenticationScheme, this.AuthenticationToken); + + await base.ProcessHttpRequestAsync(request, cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Utils.cs b/src/Utils.cs new file mode 100644 index 0000000..d99afe6 --- /dev/null +++ b/src/Utils.cs @@ -0,0 +1,86 @@ +namespace k8s +{ + using System; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Text; + using System.Threading.Tasks; + + public static class Utils + { + /// + /// Encode string in base64 format. + /// + /// string to be encoded. + /// Encoded string. + public static string Base64Encode(string text) + { + return Convert.ToBase64String(Encoding.UTF8.GetBytes(text)); + } + + /// + /// Encode string in base64 format. + /// + /// string to be encoded. + /// Encoded string. + public static string Base64Decode(string text) + { + return Encoding.UTF8.GetString(Convert.FromBase64String(text)); + } + + /// + /// Generates pfx from client configuration + /// + /// Kuberentes Client Configuration + /// Generated Pfx Path + /// TODO: kabhishek8260 Remplace the method with X509 Certificate with private key(in dotnet 2.0) + public static async Task GeneratePfxAsync(KubernetesClientConfiguration config) + { + var userHomeDir = Environment.GetEnvironmentVariable("HOME"); + var certDirPath = Path.Combine(userHomeDir, ".k8scerts"); + Directory.CreateDirectory(certDirPath); + + var filePrefix = config.CurrentContext; + var keyFilePath = Path.Combine(certDirPath, filePrefix + "key"); + var certFilePath = Path.Combine(certDirPath, filePrefix + "cert"); + var pfxFilePath = Path.Combine(certDirPath, filePrefix + "pfx"); + + using (FileStream fs = File.Create(keyFilePath)) + { + byte[] info = Convert.FromBase64String(config.ClientCertificateKey); + await fs.WriteAsync(info, 0, info.Length).ConfigureAwait(false); + } + + using (FileStream fs = File.Create(certFilePath)) + { + byte[] info = Convert.FromBase64String(config.ClientCertificateData); + await fs.WriteAsync(info, 0, info.Length).ConfigureAwait(false); + } + + var process = new Process(); + process.StartInfo = new ProcessStartInfo() + { + FileName = @"/bin/bash", + Arguments = string.Format( + CultureInfo.InvariantCulture, + "-c \"openssl pkcs12 -export -out {0} -inkey {1} -in {2} -passout pass:\"", + pfxFilePath, + keyFilePath, + certFilePath), + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + process.Start(); + process.WaitForExit(); + if (process.ExitCode == 0) + { + return pfxFilePath; + } + + return null; + } + } +}