Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/KubernetesClient.Classic/CertUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using k8s.Exceptions;
using Org.BouncyCastle.Asn1.Nist;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace k8s
{
internal static class CertUtils
{
/// <summary>
/// Load pem encoded cert file
/// </summary>
/// <param name="file">Path to pem encoded cert file</param>
/// <returns>List of x509 instances.</returns>
public static X509Certificate2Collection LoadPemFileCert(string file)
{
var certCollection = new X509Certificate2Collection();
using (var stream = FileSystem.Current.OpenRead(file))
{
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;
}

/// <summary>
/// Generates pfx from client configuration
/// </summary>
/// <param name="config">Kubernetes Client Configuration</param>
/// <returns>Generated Pfx Path</returns>
public static X509Certificate2 GeneratePfx(KubernetesClientConfiguration config)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

byte[] keyData = null;
byte[] certData = null;

if (!string.IsNullOrWhiteSpace(config.ClientCertificateKeyData))
{
keyData = Convert.FromBase64String(config.ClientCertificateKeyData);
}

if (!string.IsNullOrWhiteSpace(config.ClientKeyFilePath))
{
keyData = File.ReadAllBytes(config.ClientKeyFilePath);
}

if (keyData == null)
{
throw new KubeConfigException("keyData is empty");
}

if (!string.IsNullOrWhiteSpace(config.ClientCertificateData))
{
certData = Convert.FromBase64String(config.ClientCertificateData);
}

if (!string.IsNullOrWhiteSpace(config.ClientCertificateFilePath))
{
certData = File.ReadAllBytes(config.ClientCertificateFilePath);
}

if (certData == null)
{
throw new KubeConfigException("certData is empty");
}

var cert = new X509CertificateParser().ReadCertificate(new MemoryStream(certData));
// key usage is a bit string, zero-th bit is 'digitalSignature'
// See https://www.alvestrand.no/objectid/2.5.29.15.html for more details.
if (cert != null && cert.GetKeyUsage() != null && !cert.GetKeyUsage()[0])
{
throw new Exception(
"Client certificates must be marked for digital signing. " +
"See https://github.com/kubernetes-client/csharp/issues/319");
}

object obj;
using (var reader = new StreamReader(new MemoryStream(keyData)))
{
obj = new PemReader(reader).ReadObject();
if (obj is AsymmetricCipherKeyPair key)
{
var cipherKey = key;
obj = cipherKey.Private;
}
}

var keyParams = (AsymmetricKeyParameter)obj;

var store = new Pkcs12StoreBuilder()
.SetKeyAlgorithm(NistObjectIdentifiers.IdAes128Cbc, PkcsObjectIdentifiers.IdHmacWithSha1)
.Build();
store.SetKeyEntry("K8SKEY", new AsymmetricKeyEntry(keyParams), new[] { new X509CertificateEntry(cert) });

using var pkcs = new MemoryStream();

store.Save(pkcs, new char[0], new SecureRandom());

// 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;

if (config.ClientCertificateKeyStoreFlags.HasValue)
{
return new X509Certificate2(pkcs.ToArray(), nullPassword, config.ClientCertificateKeyStoreFlags.Value);
}
else
{
return new X509Certificate2(pkcs.ToArray(), nullPassword);
}
}

/// <summary>
/// Retrieves Client Certificate PFX from configuration
/// </summary>
/// <param name="config">Kubernetes Client Configuration</param>
/// <returns>Client certificate PFX</returns>
public static X509Certificate2 GetClientCert(KubernetesClientConfiguration config)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}

if ((!string.IsNullOrWhiteSpace(config.ClientCertificateData) ||
!string.IsNullOrWhiteSpace(config.ClientCertificateFilePath)) &&
(!string.IsNullOrWhiteSpace(config.ClientCertificateKeyData) ||
!string.IsNullOrWhiteSpace(config.ClientKeyFilePath)))
{
return GeneratePfx(config);
}

return null;
}
}
}
5 changes: 2 additions & 3 deletions src/KubernetesClient.Classic/KubernetesClient.Classic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.3" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.2.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.0" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
</ItemGroup>
Expand All @@ -18,7 +18,6 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\KubernetesClient\CertUtils.cs" />
<Compile Include="..\KubernetesClient\FileSystem.cs" />
<Compile Include="..\KubernetesClient\IKubernetes.cs" />
<Compile Include="..\KubernetesClient\Kubernetes.ConfigInit.cs" />
Expand Down
102 changes: 0 additions & 102 deletions src/KubernetesClient/CertUtils.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
using k8s.Exceptions;
#if !NET5_0_OR_GREATER
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
#else
using System.Runtime.InteropServices;
using System.Text;
#endif
using System.IO;
using System.Security.Cryptography.X509Certificates;

Expand All @@ -26,22 +18,7 @@ public static X509Certificate2Collection LoadPemFileCert(string file)
var certCollection = new X509Certificate2Collection();
using (var stream = FileSystem.Current.OpenRead(file))
{
#if NET5_0_OR_GREATER
certCollection.ImportFromPem(new StreamReader(stream).ReadToEnd());
#else
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));
}
#endif
}

return certCollection;
Expand All @@ -59,7 +36,6 @@ public static X509Certificate2 GeneratePfx(KubernetesClientConfiguration config)
throw new ArgumentNullException(nameof(config));
}

#if NET5_0_OR_GREATER
string keyData = null;
string certData = null;

Expand Down Expand Up @@ -114,84 +90,6 @@ public static X509Certificate2 GeneratePfx(KubernetesClientConfiguration config)
}

return cert;
#else

byte[] keyData = null;
byte[] certData = null;

if (!string.IsNullOrWhiteSpace(config.ClientCertificateKeyData))
{
keyData = Convert.FromBase64String(config.ClientCertificateKeyData);
}

if (!string.IsNullOrWhiteSpace(config.ClientKeyFilePath))
{
keyData = File.ReadAllBytes(config.ClientKeyFilePath);
}

if (keyData == null)
{
throw new KubeConfigException("keyData is empty");
}

if (!string.IsNullOrWhiteSpace(config.ClientCertificateData))
{
certData = Convert.FromBase64String(config.ClientCertificateData);
}

if (!string.IsNullOrWhiteSpace(config.ClientCertificateFilePath))
{
certData = File.ReadAllBytes(config.ClientCertificateFilePath);
}

if (certData == null)
{
throw new KubeConfigException("certData is empty");
}

var cert = new X509CertificateParser().ReadCertificate(new MemoryStream(certData));
// key usage is a bit string, zero-th bit is 'digitalSignature'
// See https://www.alvestrand.no/objectid/2.5.29.15.html for more details.
if (cert != null && cert.GetKeyUsage() != null && !cert.GetKeyUsage()[0])
{
throw new Exception(
"Client certificates must be marked for digital signing. " +
"See https://github.com/kubernetes-client/csharp/issues/319");
}

object obj;
using (var reader = new StreamReader(new MemoryStream(keyData)))
{
obj = new PemReader(reader).ReadObject();
if (obj is AsymmetricCipherKeyPair key)
{
var cipherKey = key;
obj = cipherKey.Private;
}
}

var keyParams = (AsymmetricKeyParameter)obj;

var store = new Pkcs12StoreBuilder().Build();
store.SetKeyEntry("K8SKEY", new AsymmetricKeyEntry(keyParams), new[] { new X509CertificateEntry(cert) });

using var pkcs = new MemoryStream();

store.Save(pkcs, new char[0], new SecureRandom());

// 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;

if (config.ClientCertificateKeyStoreFlags.HasValue)
{
return new X509Certificate2(pkcs.ToArray(), nullPassword, config.ClientCertificateKeyStoreFlags.Value);
}
else
{
return new X509Certificate2(pkcs.ToArray(), nullPassword);
}
#endif
}

/// <summary>
Expand Down