Add bundle certificate support (#253)

* initial

* add some comments

* cleanup

* var

* Use X509Certificate2cCollection

* add missing asset files

* address comments
This commit is contained in:
etchang
2019-03-11 06:39:28 -07:00
committed by Kubernetes Prow Robot
parent f6b58d535a
commit 9bdaf132d4
14 changed files with 202 additions and 59 deletions

View File

@@ -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
{
@@ -19,22 +18,21 @@ namespace k8s
/// 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)
var certs = new X509CertificateParser().ReadCertificates(File.OpenRead(file));
var certCollection = new X509Certificate2Collection();
// Convert BouncyCastle X509Certificates to the .NET cryptography implementation and add
// it to the certificate collection
//
foreach (Org.BouncyCastle.X509.X509Certificate cert in certs)
{
store.SetCertificateEntry(Guid.NewGuid().ToString(), new X509CertificateEntry(cert));
certCollection.Add(new X509Certificate2(cert.GetEncoded()));
}
using (var pkcs = new MemoryStream())
{
store.Save(pkcs, new char[0], new SecureRandom());
// TODO not a chain
return new X509Certificate2(pkcs.ToArray());
}
return certCollection;
}
/// <summary>

View File

@@ -39,7 +39,7 @@ namespace k8s
throw new KubeConfigException("Bad host url", e);
}
CaCert = config.SslCaCert;
CaCerts = config.SslCaCerts;
SkipTlsVerify = config.SkipTlsVerify;
if (BaseUri.Scheme == "https")
@@ -53,21 +53,26 @@ namespace k8s
}
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;
}
}
@@ -100,7 +105,7 @@ namespace k8s
throw new KubeConfigException("Bad host url", e);
}
CaCert = config.SslCaCert;
CaCerts = config.SslCaCerts;
SkipTlsVerify = config.SkipTlsVerify;
if (BaseUri.Scheme == "https")
@@ -122,7 +127,7 @@ namespace k8s
}
else
{
if (CaCert == null)
if (CaCerts == null)
{
throw new KubeConfigException("a CA must be set when SkipTlsVerify === false");
}
@@ -130,18 +135,18 @@ namespace k8s
#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
}
@@ -152,7 +157,7 @@ namespace k8s
}
#endif
private X509Certificate2 CaCert { get; }
private X509Certificate2Collection CaCerts { get; }
private bool SkipTlsVerify { get; }
@@ -241,7 +246,7 @@ namespace k8s
[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)
@@ -257,15 +262,29 @@ namespace k8s
{
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 rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
isValid = isValid && rootCert.RawData.SequenceEqual(caCert.RawData);
var isTrusted = false;
return isValid;
var rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
// Make sure that one of our trusted certs exists in the chain provided by the server.
//
foreach (var cert in caCerts)
{
if (rootCert.RawData.SequenceEqual(cert.RawData))
{
isTrusted = true;
break;
}
}
return isValid && isTrusted;
}
// In all other cases, return false.

View File

@@ -260,16 +260,16 @@ namespace k8s
}
#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)
@@ -347,7 +347,7 @@ namespace k8s
ServiceClientTracing.Exit(invocationId, null);
}
#if NET452
if (this.CaCert != null)
if (this.CaCerts != null)
{
webSocketBuilder.CleanupServerCertificateValidationCallback(this.ServerCertificateValidationCallback);
}
@@ -359,7 +359,7 @@ namespace k8s
#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
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -189,17 +190,17 @@ namespace k8s
{
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)));
}
}
}

View File

@@ -43,7 +43,7 @@ namespace k8s
{
Host = new UriBuilder("https", host, Convert.ToInt32(port)).ToString(),
AccessToken = token,
SslCaCert = CertUtils.LoadPemFileCert(rootCAFile)
SslCaCerts = CertUtils.LoadPemFileCert(rootCAFile)
};
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
namespace k8s
@@ -18,9 +19,9 @@ namespace k8s
public string Host { get; set; }
/// <summary>
/// Gets SslCaCert
/// Gets SslCaCerts
/// </summary>
public X509Certificate2 SslCaCert { get; set; }
public X509Certificate2Collection SslCaCerts { get; set; }
/// <summary>
/// Gets ClientCertificateData

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
#if NET452
using System.Net.Security;
@@ -52,7 +53,7 @@ namespace k8s
#endif
#if NETCOREAPP2_1
public WebSocketBuilder ExpectServerCertificate(X509Certificate2 serverCertificate)
public WebSocketBuilder ExpectServerCertificate(X509Certificate2Collection serverCertificate)
{
Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -221,7 +222,7 @@ namespace k8s.Tests
Host = server.Uri.ToString(),
ClientCertificateData = clientCertificateData,
ClientCertificateKeyData = clientCertificateKeyData,
SslCaCert = serverCertificate,
SslCaCerts = new X509Certificate2Collection(serverCertificate),
SkipTlsVerify = false
});

View File

@@ -2,6 +2,9 @@ using System;
using Xunit;
using k8s;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Linq;
namespace k8s.Tests
{
@@ -72,13 +75,20 @@ namespace k8s.Tests
}
/// <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));
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
@@ -11,7 +12,7 @@ namespace k8s.Tests
[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;
@@ -24,7 +25,36 @@ namespace k8s.Tests
[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;

View File

@@ -79,7 +79,7 @@ namespace k8s.Tests
var fi = new FileInfo("assets/kubeconfig.yml");
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi, context);
Assert.Equal(context, cfg.CurrentContext);
Assert.NotNull(cfg.SslCaCert);
Assert.NotNull(cfg.SslCaCerts);
Assert.Equal(File.ReadAllText("assets/client-certificate-data.txt"), cfg.ClientCertificateData);
Assert.Equal(File.ReadAllText("assets/client-key-data.txt"), cfg.ClientCertificateKeyData);
}
@@ -94,7 +94,7 @@ namespace k8s.Tests
var fi = new FileInfo("assets/kubeconfig.tls-skip.yml");
var cfg = KubernetesClientConfiguration.BuildConfigFromConfigFile(fi);
Assert.NotNull(cfg.Host);
Assert.Null(cfg.SslCaCert);
Assert.Null(cfg.SslCaCerts);
Assert.True(cfg.SkipTlsVerify);
}

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDDTCCAfWgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKa3Vi
ZXJuZXRlczAeFw0xOTAzMDMxNzA4MDlaFw0yOTAyMjgxNzA4MDlaMBYxFDASBgNV
BAMMC2V0Y2hhbmctdWI1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
x1Tp7Da3NbjdHmYdYZ/GNpCRGvFFap7EG1pokhfILKSbPusqiO9wnKDE4Afdn/ZE
CQV0Whwtox3jczBOIRy+P6FvlPyhApUpynVTwgCiuhTM+thgODgpe6GXmVlVJGvv
AoLw7CMndB5sMs5HH+qA2U1q4VFI/csr3/yeKzWBik3dZVoh04sI9WTVL+bl/1X5
0dl5qrqkYiDx8ycAHyOnl8dhJW+RGl67HiliuUeSq6vwsfv9rh3TP9wHVF1PXFJp
WfXy4WbLmuld5wxXnQVO2g51jqfqN9fD8FHIkae1IkO/PUTucloNlLiFsragQOTD
RVSP+TV3gshATBs2MMVXMwIDAQABo2YwZDAdBgNVHQ4EFgQU/3w9AR2cnEepWH4E
8a1xLZAnjykwHwYDVR0jBBgwFoAULs/lzct8CGvVdIiq4t9T4idu5OwwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEB
AKw741V1wszIthHBV8dvCyQoyozBJuAo4IHbiiFmzuiQuyshMcX+Qs9a+g6OG5d1
UbwFfUlqzmZQcbcR/Jc6wMz3wO6Hoy5pS3w/FR2UMGR39o95/7XCkTIOwCqau6Pw
dpgvbnaiqPFPqD3ohdUuVRcXG3va5AmKTsUn7m+lR/93/qptt+SUVp6jwnbGcwoB
s3u2XXx5s1M7tqqj3tAEOPCKlohS6mQ4X3wulgpZ1XpJ0WTvcvoPXEtA56k7vX3a
4E6x66LZCFA2ZR/5COv5D055AhrihKL8kbAutxhfA27SJ/MGowzmTT7kVQha3Su3
aoOYZgcUww+SkRSGVrtgMgQ=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDEDCCAfigAwIBAgIJAJpb9irKg2JjMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV
BAMMCmt1YmVybmV0ZXMwHhcNMTkwMzAzMDAyMTI3WhcNMzkwMjI2MDAyMTI3WjAV
MRMwEQYDVQQDDAprdWJlcm5ldGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAuoK+Na+LyKIipmftBw4Y+Z19g8yv6Y+dYt6KlBg24XHNA0v1cMwtOCDF
Mlm4rsD7Jd5UO6ugdk3fxEvGGhmzWTXSBRUWcTbScAM49mALBFkCvNTPK2vVhk7P
im2QQl8a5vjYz8HLKJTb/O+0q+Kktpd7XTaU2U7ZebiLVs5bvNbb3ZDtIjAARY9S
alZ4hOzuVNaSX9MBRqTWq3HuKwDiVTT3dan/ABoU8NdedPfIbyY48wiQgjEYb64g
3geYpArLQeffo8fmhUEPRR/1WrfvYvvm8sV8jT+rqxITKJ5Vo5kpZUpomDOtGVMS
gGAle6mcTrqlrsCFc4gFRRoHiH1ODQIDAQABo2MwYTAdBgNVHQ4EFgQULs/lzct8
CGvVdIiq4t9T4idu5OwwHwYDVR0jBBgwFoAULs/lzct8CGvVdIiq4t9T4idu5Oww
DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD
ggEBAFMq5z4OoaIhqx/i/btpVLRnQDcpDwdUurE0iNPz3bgOI5QiIe95oUwXSFQL
ciECvObfMmiVuz+p7ND5eNdxYR4hlq1W1PYcgRQgusXCC4Xd/XAGaLZXzH3SBrmp
bs5sfokhXKNccsnQu5Ya3JRkALxxUJ+DcOn+vi9gmEAzi+nXbyqUjIhSD5nygClX
0aSKbvhUmXyaJpUH0i7dSxWP3LqCDjtru/5ejNtB097dNcyF8js3Yuk3hwqyegQx
ELn4c/TKPL9L8vE7tJg/M78DPAvRCiuwl0HQcasBE2AX0wdpY0UeXsNDyzUf/2WF
fHY4DnuBdeVdHtl1yPlXmQkMoQM=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,44 @@
--- Comments to make sure we still parse the cert correctly
Intermediate Certificate
-----BEGIN CERTIFICATE-----
MIIDDTCCAfWgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UEAwwKa3Vi
ZXJuZXRlczAeFw0xOTAzMDMxNzA4MDlaFw0yOTAyMjgxNzA4MDlaMBYxFDASBgNV
BAMMC2V0Y2hhbmctdWI1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
x1Tp7Da3NbjdHmYdYZ/GNpCRGvFFap7EG1pokhfILKSbPusqiO9wnKDE4Afdn/ZE
CQV0Whwtox3jczBOIRy+P6FvlPyhApUpynVTwgCiuhTM+thgODgpe6GXmVlVJGvv
AoLw7CMndB5sMs5HH+qA2U1q4VFI/csr3/yeKzWBik3dZVoh04sI9WTVL+bl/1X5
0dl5qrqkYiDx8ycAHyOnl8dhJW+RGl67HiliuUeSq6vwsfv9rh3TP9wHVF1PXFJp
WfXy4WbLmuld5wxXnQVO2g51jqfqN9fD8FHIkae1IkO/PUTucloNlLiFsragQOTD
RVSP+TV3gshATBs2MMVXMwIDAQABo2YwZDAdBgNVHQ4EFgQU/3w9AR2cnEepWH4E
8a1xLZAnjykwHwYDVR0jBBgwFoAULs/lzct8CGvVdIiq4t9T4idu5OwwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEB
AKw741V1wszIthHBV8dvCyQoyozBJuAo4IHbiiFmzuiQuyshMcX+Qs9a+g6OG5d1
UbwFfUlqzmZQcbcR/Jc6wMz3wO6Hoy5pS3w/FR2UMGR39o95/7XCkTIOwCqau6Pw
dpgvbnaiqPFPqD3ohdUuVRcXG3va5AmKTsUn7m+lR/93/qptt+SUVp6jwnbGcwoB
s3u2XXx5s1M7tqqj3tAEOPCKlohS6mQ4X3wulgpZ1XpJ0WTvcvoPXEtA56k7vX3a
4E6x66LZCFA2ZR/5COv5D055AhrihKL8kbAutxhfA27SJ/MGowzmTT7kVQha3Su3
aoOYZgcUww+SkRSGVrtgMgQ=
-----END CERTIFICATE-----
Root certificate
-----BEGIN CERTIFICATE-----
MIIDEDCCAfigAwIBAgIJAJpb9irKg2JjMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV
BAMMCmt1YmVybmV0ZXMwHhcNMTkwMzAzMDAyMTI3WhcNMzkwMjI2MDAyMTI3WjAV
MRMwEQYDVQQDDAprdWJlcm5ldGVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAuoK+Na+LyKIipmftBw4Y+Z19g8yv6Y+dYt6KlBg24XHNA0v1cMwtOCDF
Mlm4rsD7Jd5UO6ugdk3fxEvGGhmzWTXSBRUWcTbScAM49mALBFkCvNTPK2vVhk7P
im2QQl8a5vjYz8HLKJTb/O+0q+Kktpd7XTaU2U7ZebiLVs5bvNbb3ZDtIjAARY9S
alZ4hOzuVNaSX9MBRqTWq3HuKwDiVTT3dan/ABoU8NdedPfIbyY48wiQgjEYb64g
3geYpArLQeffo8fmhUEPRR/1WrfvYvvm8sV8jT+rqxITKJ5Vo5kpZUpomDOtGVMS
gGAle6mcTrqlrsCFc4gFRRoHiH1ODQIDAQABo2MwYTAdBgNVHQ4EFgQULs/lzct8
CGvVdIiq4t9T4idu5OwwHwYDVR0jBBgwFoAULs/lzct8CGvVdIiq4t9T4idu5Oww
DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD
ggEBAFMq5z4OoaIhqx/i/btpVLRnQDcpDwdUurE0iNPz3bgOI5QiIe95oUwXSFQL
ciECvObfMmiVuz+p7ND5eNdxYR4hlq1W1PYcgRQgusXCC4Xd/XAGaLZXzH3SBrmp
bs5sfokhXKNccsnQu5Ya3JRkALxxUJ+DcOn+vi9gmEAzi+nXbyqUjIhSD5nygClX
0aSKbvhUmXyaJpUH0i7dSxWP3LqCDjtru/5ejNtB097dNcyF8js3Yuk3hwqyegQx
ELn4c/TKPL9L8vE7tJg/M78DPAvRCiuwl0HQcasBE2AX0wdpY0UeXsNDyzUf/2WF
fHY4DnuBdeVdHtl1yPlXmQkMoQM=
-----END CERTIFICATE-----