Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bundle certificate support #253

Merged
merged 7 commits into from
Mar 11, 2019
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
34 changes: 16 additions & 18 deletions src/KubernetesClient/CertUtils.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using k8s.Exceptions;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;

namespace k8s
{
Expand All @@ -19,22 +18,21 @@ public static class CertUtils
/// Load pem encoded cert file
/// </summary>
/// <param name="file">Path to pem encoded cert file</param>
/// <returns>x509 instance.</returns>
public static X509Certificate2 LoadPemFileCert(string file)
/// <returns>List of x509 instances.</returns>
public static X509Certificate2Collection LoadPemFileCert(string file)
{
var certs = new X509CertificateParser().ReadCertificates(File.OpenRead(file));
var store = new Pkcs12StoreBuilder().Build();
foreach (X509Certificate cert in certs)
tg123 marked this conversation as resolved.
Show resolved Hide resolved
{
store.SetCertificateEntry(Guid.NewGuid().ToString(), new X509CertificateEntry(cert));
}
var certs = new X509CertificateParser().ReadCertificates(File.OpenRead(file));
var certCollection = new X509Certificate2Collection();

using (var pkcs = new MemoryStream())
// Convert BouncyCastle X509Certificates to the .NET cryptography implementation and add
// it to the certificate collection
//
foreach (Org.BouncyCastle.X509.X509Certificate cert in certs)
{
store.Save(pkcs, new char[0], new SecureRandom());
// TODO not a chain
return new X509Certificate2(pkcs.ToArray());
certCollection.Add(new X509Certificate2(cert.GetEncoded()));
}

return certCollection;
}

/// <summary>
Expand Down
59 changes: 39 additions & 20 deletions src/KubernetesClient/Kubernetes.ConfigInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler
throw new KubeConfigException("Bad host url", e);
}

CaCert = config.SslCaCert;
CaCerts = config.SslCaCerts;
SkipTlsVerify = config.SkipTlsVerify;

if (BaseUri.Scheme == "https")
Expand All @@ -53,21 +53,26 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler
}
else
{
if (CaCert == null)
if (CaCerts == null)
{
throw new KubeConfigException("a CA must be set when SkipTlsVerify === false");
}

using (System.IO.MemoryStream certStream = new System.IO.MemoryStream(config.SslCaCert.RawData))
{
Java.Security.Cert.Certificate cert = Java.Security.Cert.CertificateFactory.GetInstance("X509").GenerateCertificate(certStream);
Xamarin.Android.Net.AndroidClientHandler handler = (Xamarin.Android.Net.AndroidClientHandler)this.HttpClientHandler;
var certList = new System.Collections.Generic.List<Java.Security.Cert.Certificate>();

handler.TrustedCerts = new System.Collections.Generic.List<Java.Security.Cert.Certificate>()
foreach (X509Certificate2 caCert in CaCerts)
{
using (System.IO.MemoryStream certStream = new System.IO.MemoryStream(caCert.RawData))
{
cert
};
Java.Security.Cert.Certificate cert = Java.Security.Cert.CertificateFactory.GetInstance("X509").GenerateCertificate(certStream);

certList.Add(cert);
}
}

Xamarin.Android.Net.AndroidClientHandler handler = (Xamarin.Android.Net.AndroidClientHandler)this.HttpClientHandler;

handler.TrustedCerts = certList;
}
}

Expand Down Expand Up @@ -100,7 +105,7 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler
throw new KubeConfigException("Bad host url", e);
}

CaCert = config.SslCaCert;
CaCerts = config.SslCaCerts;
SkipTlsVerify = config.SkipTlsVerify;

if (BaseUri.Scheme == "https")
Expand All @@ -122,26 +127,26 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler
}
else
{
if (CaCert == null)
if (CaCerts == null)
{
throw new KubeConfigException("a CA must be set when SkipTlsVerify === false");
}

#if NET452
((WebRequestHandler) HttpClientHandler).ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
return Kubernetes.CertificateValidationCallBack(sender, CaCert, certificate, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, CaCerts, certificate, chain, sslPolicyErrors);
};
#elif XAMARINIOS1_0
System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{
var cert = new X509Certificate2(certificate);
return Kubernetes.CertificateValidationCallBack(sender, CaCert, cert, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, CaCerts, cert, chain, sslPolicyErrors);
};
#else
HttpClientHandler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
return Kubernetes.CertificateValidationCallBack(sender, CaCert, certificate, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, CaCerts, certificate, chain, sslPolicyErrors);
};
#endif
}
Expand All @@ -152,7 +157,7 @@ public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler
}
#endif

private X509Certificate2 CaCert { get; }
private X509Certificate2Collection CaCerts { get; }

private bool SkipTlsVerify { get; }

Expand Down Expand Up @@ -241,7 +246,7 @@ private void SetCredentials(KubernetesClientConfiguration config, HttpClientHand
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Unused by design")]
public static bool CertificateValidationCallBack(
object sender,
X509Certificate2 caCert,
X509Certificate2Collection caCerts,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
Expand All @@ -257,15 +262,29 @@ public static bool CertificateValidationCallBack(
{
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

// add all your extra certificate chain
chain.ChainPolicy.ExtraStore.Add(caCert);
// Added our trusted certificates to the chain
//
chain.ChainPolicy.ExtraStore.AddRange(caCerts);

chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
var isValid = chain.Build((X509Certificate2)certificate);

var isTrusted = false;

var rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
isValid = isValid && rootCert.RawData.SequenceEqual(caCert.RawData);

return isValid;
// Make sure that one of our trusted certs exists in the chain provided by the server.
//
foreach (var cert in caCerts)
tg123 marked this conversation as resolved.
Show resolved Hide resolved
{
if (rootCert.RawData.SequenceEqual(cert.RawData))
{
isTrusted = true;
break;
}
}

return isValid && isTrusted;
}

// In all other cases, return false.
Expand Down
10 changes: 5 additions & 5 deletions src/KubernetesClient/Kubernetes.WebSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,16 +260,16 @@ public partial class Kubernetes
}

#if NET452
if (this.CaCert != null)
if (this.CaCerts != null)
{
webSocketBuilder.SetServerCertificateValidationCallback(this.ServerCertificateValidationCallback);
}
#endif

#if NETCOREAPP2_1
if (this.CaCert != null)
if (this.CaCerts != null)
{
webSocketBuilder.ExpectServerCertificate(this.CaCert);
webSocketBuilder.ExpectServerCertificate(this.CaCerts);
}

if (this.SkipTlsVerify)
Expand Down Expand Up @@ -347,7 +347,7 @@ public partial class Kubernetes
ServiceClientTracing.Exit(invocationId, null);
}
#if NET452
if (this.CaCert != null)
if (this.CaCerts != null)
{
webSocketBuilder.CleanupServerCertificateValidationCallback(this.ServerCertificateValidationCallback);
}
Expand All @@ -359,7 +359,7 @@ public partial class Kubernetes
#if NET452
internal bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return Kubernetes.CertificateValidationCallBack(sender, this.CaCert, certificate, chain, sslPolicyErrors);
return Kubernetes.CertificateValidationCallBack(sender, this.CaCerts, certificate, chain, sslPolicyErrors);
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -189,17 +190,17 @@ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext
{
throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)");
}

if (uri.Scheme == "https")
{
if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
{
var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
SslCaCert = new X509Certificate2(Convert.FromBase64String(data));
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(data)));
}
else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
{
SslCaCert = new X509Certificate2(GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority));
SslCaCerts = new X509Certificate2Collection(new X509Certificate2(GetFullPath(k8SConfig, clusterDetails.ClusterEndpoint.CertificateAuthority)));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static KubernetesClientConfiguration InClusterConfig()
{
Host = new UriBuilder("https", host, Convert.ToInt32(port)).ToString(),
AccessToken = token,
SslCaCert = CertUtils.LoadPemFileCert(rootCAFile)
SslCaCerts = CertUtils.LoadPemFileCert(rootCAFile)
};
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/KubernetesClient/KubernetesClientConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;

namespace k8s
Expand All @@ -18,9 +19,9 @@ public partial class KubernetesClientConfiguration
public string Host { get; set; }

/// <summary>
/// Gets SslCaCert
/// Gets SslCaCerts
/// </summary>
public X509Certificate2 SslCaCert { get; set; }
public X509Certificate2Collection SslCaCerts { get; set; }

/// <summary>
/// Gets ClientCertificateData
Expand Down
3 changes: 2 additions & 1 deletion src/KubernetesClient/WebSocketBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
#if NET452
using System.Net.Security;
Expand Down Expand Up @@ -52,7 +53,7 @@ public void CleanupServerCertificateValidationCallback(RemoteCertificateValidati
#endif

#if NETCOREAPP2_1
public WebSocketBuilder ExpectServerCertificate(X509Certificate2 serverCertificate)
public WebSocketBuilder ExpectServerCertificate(X509Certificate2Collection serverCertificate)
{
Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
Expand Down
3 changes: 2 additions & 1 deletion tests/KubernetesClient.Tests/AuthTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -221,7 +222,7 @@ public void Cert()
Host = server.Uri.ToString(),
ClientCertificateData = clientCertificateData,
ClientCertificateKeyData = clientCertificateKeyData,
SslCaCert = serverCertificate,
SslCaCerts = new X509Certificate2Collection(serverCertificate),
SkipTlsVerify = false
});

Expand Down
16 changes: 13 additions & 3 deletions tests/KubernetesClient.Tests/CertUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using Xunit;
using k8s;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Linq;

namespace k8s.Tests
{
Expand Down Expand Up @@ -72,13 +75,20 @@ public void LoadFromInlineDataRelativePath()
}

/// <summary>
/// Checks
/// Checks that the bundle certificate was loaded correctly
/// </summary>
[Fact]
public void LoadPemWithMultiCert()
{
var cert = CertUtils.LoadPemFileCert("assets/ca3.crt");
Assert.NotNull(cert.PublicKey);
var certCollection = CertUtils.LoadPemFileCert("assets/ca-bundle.crt");

var intermediateCert = new X509Certificate2("assets/ca-bundle-intermediate.crt");
var rootCert = new X509Certificate2("assets/ca-bundle-root.crt");

Assert.Equal(2, certCollection.Count);

Assert.True(certCollection[0].RawData.SequenceEqual(intermediateCert.RawData));
Assert.True(certCollection[1].RawData.SequenceEqual(rootCert.RawData));
}
}
}
34 changes: 32 additions & 2 deletions tests/KubernetesClient.Tests/CertificateValidationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
Expand All @@ -11,7 +12,7 @@ public class CertificateValidationTests
[Fact]
public void ValidCert()
{
var caCert = new X509Certificate2("assets/ca.crt");
var caCert = CertUtils.LoadPemFileCert("assets/ca.crt");
var testCert = new X509Certificate2("assets/ca.crt");
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;
Expand All @@ -24,7 +25,36 @@ public void ValidCert()
[Fact]
public void InvalidCert()
{
var caCert = new X509Certificate2("assets/ca.crt");
var caCert = CertUtils.LoadPemFileCert("assets/ca.crt");
var testCert = new X509Certificate2("assets/ca2.crt");
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;

var result = Kubernetes.CertificateValidationCallBack(this, caCert, testCert, chain, errors);

Assert.False(result);
}

[Fact]
public void ValidBundleCert()
{
var caCert = CertUtils.LoadPemFileCert("assets/ca-bundle.crt");

// Load the intermediate cert
//
var testCert = caCert[0];
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;

var result = Kubernetes.CertificateValidationCallBack(this, caCert, testCert, chain, errors);

Assert.True(result);
}

[Fact]
public void InvalidBundleCert()
{
var caCert = CertUtils.LoadPemFileCert("assets/ca-bundle.crt");
var testCert = new X509Certificate2("assets/ca2.crt");
var chain = new X509Chain();
var errors = SslPolicyErrors.RemoteCertificateChainErrors;
Expand Down
Loading