diff --git a/src/KubernetesClient.Classic/CertUtils.cs b/src/KubernetesClient.Classic/CertUtils.cs index 112ef922e..21bebebba 100644 --- a/src/KubernetesClient.Classic/CertUtils.cs +++ b/src/KubernetesClient.Classic/CertUtils.cs @@ -7,6 +7,7 @@ using Org.BouncyCastle.Security; using Org.BouncyCastle.X509; using System.Security.Cryptography.X509Certificates; +using System.Text; namespace k8s { @@ -39,6 +40,33 @@ public static X509Certificate2Collection LoadPemFileCert(string file) return certCollection; } + /// + /// Load pem encoded certificates from text + /// + /// PEM encoded certificate text + /// List of x509 instances. + public static X509Certificate2Collection LoadFromPemText(string pemText) + { + var certCollection = new X509Certificate2Collection(); + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(pemText))) + { + var certs = new X509CertificateParser().ReadCertificates(stream); + + // Convert BouncyCastle X509Certificates to the .NET cryptography implementation and add + // it to the certificate collection + // + foreach (Org.BouncyCastle.X509.X509Certificate cert in certs) + { + // This null password is to change the constructor to fix this KB: + // https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b + string nullPassword = null; + certCollection.Add(new X509Certificate2(cert.GetEncoded(), nullPassword)); + } + } + + return certCollection; + } + /// /// Generates pfx from client configuration /// diff --git a/src/KubernetesClient/CertUtils.cs b/src/KubernetesClient/CertUtils.cs index 7a398d7e8..1f1a2180c 100644 --- a/src/KubernetesClient/CertUtils.cs +++ b/src/KubernetesClient/CertUtils.cs @@ -23,6 +23,18 @@ public static X509Certificate2Collection LoadPemFileCert(string file) return certCollection; } + /// + /// Load pem encoded certificates from text + /// + /// PEM encoded certificate text + /// List of x509 instances. + public static X509Certificate2Collection LoadFromPemText(string pemText) + { + var certCollection = new X509Certificate2Collection(); + certCollection.ImportFromPem(pemText); + return certCollection; + } + /// /// Generates pfx from client configuration /// diff --git a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs index f25c55bbb..ca9e206bd 100644 --- a/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs +++ b/src/KubernetesClient/KubernetesClientConfiguration.ConfigFile.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Net; using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; using System.Text; namespace k8s @@ -308,26 +307,14 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData)) { var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData; -#if NET9_0_OR_GREATER - SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(Convert.FromBase64String(data))); -#else - string nullPassword = null; - // This null password is to change the constructor to fix this KB: - // https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b - SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(data), nullPassword)); -#endif + var pemText = Encoding.UTF8.GetString(Convert.FromBase64String(data)); + SslCaCerts = CertUtils.LoadFromPemText(pemText); } else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority)) { -#if NET9_0_OR_GREATER - SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificateFromFile(GetFullPath( + SslCaCerts = CertUtils.LoadPemFileCert(GetFullPath( k8SConfig, - clusterDetails.ClusterEndpoint.CertificateAuthority))); -#else - SslCaCerts = new X509Certificate2Collection(new X509Certificate2(GetFullPath( - k8SConfig, - clusterDetails.ClusterEndpoint.CertificateAuthority))); -#endif + clusterDetails.ClusterEndpoint.CertificateAuthority)); } } } diff --git a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs index a1239c9e9..397c4f431 100644 --- a/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs +++ b/tests/KubernetesClient.Tests/KubernetesClientConfigurationTests.cs @@ -822,5 +822,31 @@ public void LoadInClusterNamespace() Assert.Equal("some namespace", config.Namespace); } } + + /// + /// Checks that multiple certificates are loaded from certificate-authority-data + /// + [Fact] + public void LoadMultipleCertificatesFromCertificateAuthorityData() + { + var fi = new FileInfo("assets/kubeconfig.multi-ca.yml"); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, "multi-ca-context"); + + Assert.NotNull(cfg.SslCaCerts); + Assert.Equal(2, cfg.SslCaCerts.Count); + } + + /// + /// Checks that multiple certificates are loaded from certificate-authority file + /// + [Fact] + public void LoadMultipleCertificatesFromCertificateAuthorityFile() + { + var fi = new FileInfo("assets/kubeconfig.multi-ca-file.yml"); + var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, "multi-ca-file-context", useRelativePaths: false); + + Assert.NotNull(cfg.SslCaCerts); + Assert.Equal(2, cfg.SslCaCerts.Count); + } } } diff --git a/tests/KubernetesClient.Tests/assets/kubeconfig.multi-ca-file.yml b/tests/KubernetesClient.Tests/assets/kubeconfig.multi-ca-file.yml new file mode 100644 index 000000000..af1340adf --- /dev/null +++ b/tests/KubernetesClient.Tests/assets/kubeconfig.multi-ca-file.yml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority: assets/ca-bundle.crt + server: https://multi-ca-file-test.example.com:6443 + name: multi-ca-file-cluster +contexts: +- context: + cluster: multi-ca-file-cluster + user: test-user + name: multi-ca-file-context +current-context: multi-ca-file-context +users: +- name: test-user + user: + token: test-token diff --git a/tests/KubernetesClient.Tests/assets/kubeconfig.multi-ca.yml b/tests/KubernetesClient.Tests/assets/kubeconfig.multi-ca.yml new file mode 100644 index 000000000..0d9f3321a --- /dev/null +++ b/tests/KubernetesClient.Tests/assets/kubeconfig.multi-ca.yml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Config +clusters: +- cluster: + certificate-authority-data: LS0tIENvbW1lbnRzIHRvIG1ha2Ugc3VyZSB3ZSBzdGlsbCBwYXJzZSB0aGUgY2VydCBjb3JyZWN0bHkKCkludGVybWVkaWF0ZSBDZXJ0aWZpY2F0ZQoKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUREVENDQWZXZ0F3SUJBZ0lDRUFFd0RRWUpLb1pJaHZjTkFRRUxCUUF3RlRFVE1CRUdBMVVFQXd3S2EzVmkKWlhKdVpYUmxjekFlRncweE9UQXpNRE14TnpBNE1EbGFGdzB5T1RBeU1qZ3hOekE0TURsYU1CWXhGREFTQmdOVgpCQU1NQzJWMFkyaGhibWN0ZFdJMU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCngxVHA3RGEzTmJqZEhtWWRZWi9HTnBDUkd2RkZhcDdFRzFwb2toZklMS1NiUHVzcWlPOXduS0RFNEFmZG4vWkUKQ1FWMFdod3RveDNqY3pCT0lSeStQNkZ2bFB5aEFwVXB5blZUd2dDaXVoVE0rdGhnT0RncGU2R1htVmxWSkd2dgpBb0x3N0NNbmRCNXNNczVISCtxQTJVMXE0VkZJL2NzcjMveWVLeldCaWszZFpWb2gwNHNJOVdUVkwrYmwvMVg1CjBkbDVxcnFrWWlEeDh5Y0FIeU9ubDhkaEpXK1JHbDY3SGlsaXVVZVNxNnZ3c2Z2OXJoM1RQOXdIVkYxUFhGSnAKV2ZYeTRXYkxtdWxkNXd4WG5RVk8yZzUxanFmcU45ZkQ4RkhJa2FlMUlrTy9QVVR1Y2xvTmxMaUZzcmFnUU9URApSVlNQK1RWM2dzaEFUQnMyTU1WWE13SURBUUFCbzJZd1pEQWRCZ05WSFE0RUZnUVUvM3c5QVIyY25FZXBXSDRFCjhhMXhMWkFuanlrd0h3WURWUjBqQkJnd0ZvQVVMcy9semN0OENHdlZkSWlxNHQ5VDRpZHU1T3d3RWdZRFZSMFQKQVFIL0JBZ3dCZ0VCL3dJQkFEQU9CZ05WSFE4QkFmOEVCQU1DQVlZd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBS3c3NDFWMXdzekl0aEhCVjhkdkN5UW95b3pCSnVBbzRJSGJpaUZtenVpUXV5c2hNY1grUXM5YStnNk9HNWQxClVid0ZmVWxxem1aUWNiY1IvSmM2d016M3dPNkhveTVwUzN3L0ZSMlVNR1IzOW85NS83WENrVElPd0NxYXU2UHcKZHBndmJuYWlxUEZQcUQzb2hkVXVWUmNYRzN2YTVBbUtUc1VuN20rbFIvOTMvcXB0dCtTVVZwNmp3bmJHY3dvQgpzM3UyWFh4NXMxTTd0cXFqM3RBRU9QQ0tsb2hTNm1RNFgzd3VsZ3BaMVhwSjBXVHZjdm9QWEV0QTU2azd2WDNhCjRFNng2NkxaQ0ZBMlpSLzVDT3Y1RDA1NUFocmloS0w4a2JBdXR4aGZBMjdTSi9NR293em1UVDdrVlFoYTNTdTMKYW9PWVpnY1V3dytTa1JTR1ZydGdNZ1E9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KClJvb3QgY2VydGlmaWNhdGUKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFRENDQWZpZ0F3SUJBZ0lKQUpwYjlpcktnMkpqTUEwR0NTcUdTSWIzRFFFQkN3VUFNQlV4RXpBUkJnTlYKQkFNTUNtdDFZbVZ5Ym1WMFpYTXdIaGNOTVRrd016QXpNREF5TVRJM1doY05Nemt3TWpJMk1EQXlNVEkzV2pBVgpNUk13RVFZRFZRUUREQXByZFdKbGNtNWxkR1Z6TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCCkNnS0NBUUVBdW9LK05hK0x5S0lpcG1mdEJ3NFkrWjE5Zzh5djZZK2RZdDZLbEJnMjRYSE5BMHYxY013dE9DREYKTWxtNHJzRDdKZDVVTzZ1Z2RrM2Z4RXZHR2hteldUWFNCUlVXY1RiU2NBTTQ5bUFMQkZrQ3ZOVFBLMnZWaGs3UAppbTJRUWw4YTV2all6OEhMS0pUYi9PKzBxK0trdHBkN1hUYVUyVTdaZWJpTFZzNWJ2TmJiM1pEdElqQUFSWTlTCmFsWjRoT3p1Vk5hU1g5TUJScVRXcTNIdUt3RGlWVFQzZGFuL0FCb1U4TmRlZFBmSWJ5WTQ4d2lRZ2pFWWI2NGcKM2dlWXBBckxRZWZmbzhmbWhVRVBSUi8xV3Jmdll2dm04c1Y4alQrcnF4SVRLSjVWbzVrcFpVcG9tRE90R1ZNUwpnR0FsZTZtY1RycWxyc0NGYzRnRlJSb0hpSDFPRFFJREFRQUJvMk13WVRBZEJnTlZIUTRFRmdRVUxzL2x6Y3Q4CkNHdlZkSWlxNHQ5VDRpZHU1T3d3SHdZRFZSMGpCQmd3Rm9BVUxzL2x6Y3Q4Q0d2VmRJaXE0dDlUNGlkdTVPd3cKRHdZRFZSMFRBUUgvQkFVd0F3RUIvekFPQmdOVkhROEJBZjhFQkFNQ0FZWXdEUVlKS29aSWh2Y05BUUVMQlFBRApnZ0VCQUZNcTV6NE9vYUlocXgvaS9idHBWTFJuUURjcER3ZFV1ckUwaU5QejNiZ09JNVFpSWU5NW9Vd1hTRlFMCmNpRUN2T2JmTW1pVnV6K3A3TkQ1ZU5keFlSNGhscTFXMVBZY2dSUWd1c1hDQzRYZC9YQUdhTFpYekgzU0JybXAKYnM1c2Zva2hYS05jY3NuUXU1WWEzSlJrQUx4eFVKK0RjT24rdmk5Z21FQXppK25YYnlxVWpJaFNENW55Z0NsWAowYVNLYnZoVW1YeWFKcFVIMGk3ZFN4V1AzTHFDRGp0cnUvNWVqTnRCMDk3ZE5jeUY4anMzWXVrM2h3cXllZ1F4CkVMbjRjL1RLUEw5TDh2RTd0SmcvTTc4RFBBdlJDaXV3bDBIUWNhc0JFMkFYMHdkcFkwVWVYc05EeXpVZi8yV0YKZkhZNERudUJkZVZkSHRsMXlQbFhtUWtNb1FNPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + server: https://multi-ca-test.example.com:6443 + name: multi-ca-cluster +contexts: +- context: + cluster: multi-ca-cluster + user: test-user + name: multi-ca-context +current-context: multi-ca-context +users: +- name: test-user + user: + token: test-token