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 client certificate chain support and SSL context generation #454

Merged
merged 13 commits into from
Apr 2, 2024
37 changes: 37 additions & 0 deletions src/NATS.Client.Core/Internal/SslStreamConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,42 @@ private SslClientAuthenticationOptions SslClientAuthenticationOptions(NatsUri ur
rcsCb = RcsCbCaCertChain;
}

#if NET8_0_OR_GREATER
X509ChainPolicy? policy = null;
SslStreamCertificateContext? streamCertificateContext = null;
if (_tlsCerts?.ClientCerts != null && _tlsCerts.CaCerts != null && _tlsCerts.ClientCerts.Count >= 1)
caleblloyd marked this conversation as resolved.
Show resolved Hide resolved
{
streamCertificateContext = SslStreamCertificateContext.Create(
_tlsCerts.ClientCerts[0],
_tlsCerts.ClientCerts,
trust: SslCertificateTrust.CreateForX509Collection(_tlsCerts.CaCerts));

policy = new()
{
RevocationMode = _tlsOpts.CertificateRevocationCheckMode,
TrustMode = X509ChainTrustMode.CustomRootTrust,
};

policy.CustomTrustStore.AddRange(_tlsCerts.CaCerts);

if (_tlsCerts.ClientCerts.Count > 1)
{
policy.ExtraStore.AddRange(_tlsCerts.ClientCerts);
}
}

var options = new SslClientAuthenticationOptions
{
TargetHost = uri.Host,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
ClientCertificates = _tlsCerts?.ClientCerts,
ClientCertificateContext = streamCertificateContext,
CertificateChainPolicy = policy,
LocalCertificateSelectionCallback = lcsCb,
RemoteCertificateValidationCallback = rcsCb,
CertificateRevocationCheckMode = _tlsOpts.CertificateRevocationCheckMode,
};
#else
var options = new SslClientAuthenticationOptions
{
TargetHost = uri.Host,
Expand All @@ -179,6 +215,7 @@ private SslClientAuthenticationOptions SslClientAuthenticationOptions(NatsUri ur
RemoteCertificateValidationCallback = rcsCb,
CertificateRevocationCheckMode = _tlsOpts.CertificateRevocationCheckMode,
};
#endif

return options;
}
Expand Down
27 changes: 18 additions & 9 deletions src/NATS.Client.Core/Internal/TlsCerts.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace NATS.Client.Core.Internal;

Expand All @@ -23,7 +25,7 @@ public static async ValueTask<TlsCerts> FromNatsTlsOptsAsync(NatsTlsOpts tlsOpts
{
case { CertFile: not null, KeyFile: null } or { KeyFile: not null, CertFile: null }:
throw new ArgumentException("NatsTlsOpts.CertFile and NatsTlsOpts.KeyFile must both be set");
case { CertFile: not null, KeyFile: not null, LoadClientCert: not null }:
case { CertFile: not null, KeyFile: not null, LoadClientCerts: not null }:
throw new ArgumentException("NatsTlsOpts.CertFile/KeyFile and NatsTlsOpts.LoadClientCert cannot both be set");
case { CaFile: not null, LoadCaCerts: not null }:
throw new ArgumentException("NatsTlsOpts.CaFile and NatsTlsOpts.LoadCaCerts cannot both be set");
Expand All @@ -42,26 +44,33 @@ public static async ValueTask<TlsCerts> FromNatsTlsOptsAsync(NatsTlsOpts tlsOpts
}

// client certificates
var clientCert = tlsOpts switch
var clientCertsProvider = tlsOpts switch
{
{ CertFile: not null, KeyFile: not null } => X509Certificate2.CreateFromPemFile(tlsOpts.CertFile, tlsOpts.KeyFile),
{ LoadClientCert: not null } => await tlsOpts.LoadClientCert().ConfigureAwait(false),
{ CertFile: not null, KeyFile: not null } => NatsTlsOpts.LoadClientCertsFromPemFile(tlsOpts.CertFile, tlsOpts.KeyFile),
{ LoadClientCerts: not null } => tlsOpts.LoadClientCerts,
_ => null,
};

if (clientCert != null)
var clientCerts = clientCertsProvider != null ? await clientCertsProvider().ConfigureAwait(false) : null;

if (clientCerts != null)
{
// On Windows, ephemeral keys/certificates do not work with schannel. e.g. unless stored in certificate store.
// https://github.com/dotnet/runtime/issues/66283#issuecomment-1061014225
// https://github.com/dotnet/runtime/blob/380a4723ea98067c28d54f30e1a652483a6a257a/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs#L192-L197
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var ephemeral = clientCert;
clientCert = new X509Certificate2(clientCert.Export(X509ContentType.Pfx));
ephemeral.Dispose();
var windowsCerts = new X509Certificate2Collection();
foreach (var ephemeralCert in clientCerts)
{
windowsCerts.Add(new X509Certificate2(ephemeralCert.Export(X509ContentType.Pfx)));
ephemeralCert.Dispose();
}

clientCerts = windowsCerts;
}

tlsCerts.ClientCerts = new X509Certificate2Collection(clientCert);
tlsCerts.ClientCerts = clientCerts;
}

return tlsCerts;
Expand Down
67 changes: 60 additions & 7 deletions src/NATS.Client.Core/NatsTlsOpts.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using NATS.Client.Core.Internal;

Expand Down Expand Up @@ -62,7 +63,7 @@ public sealed record NatsTlsOpts
/// <summary>
/// Callback that loads Client Certificate
/// </summary>
public Func<ValueTask<X509Certificate2>>? LoadClientCert { get; init; }
public Func<ValueTask<X509Certificate2Collection>>? LoadClientCerts { get; init; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should necessarily pluralize LoadClientCerts - it is still just 1 cert plus intermediates. Plural to me indicates the list of Client Certs will be tried based off some property of the cert such as common name or thumbprint. Should expand the doc here to explain why it's X509Certificate2Collection

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - should this callback return a SslStreamCertificateContext instead of an X509Certificate2Collection, since it appears that is the correct API for building a client certificate chain with intermediates

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about X509ChainPolicy it needs that as well

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SslClientAuthenticationOptions.CertificateChainPolicy applies to remote certificate validation, I think that could be implemented similar to NatsTlsOpts.CertificateRevocationCheckMode and is not needed in this callback?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard for me to weigh in on this with my limited vision of customers beyond my organization (and what they need). The most extreme and simple would be to expose the hook for SslClientAuthenticationOptions or for a Func<SslClientAuthenticationOptions,SslClientAuthenticationOptions> which allows for customizable overrides of the default built in SslStreamConnection. Between that and the current state just seems like different levels of tradeoff between flexibility and consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, trying the tests without CertificateChainPolicy doesn't seem to make any difference. Also looks like it might mess up the OCPS (I might've remembered the acronym wrong) feature:

If not null, CertificateRevocationCheckMode and SslCertificateTrust are ignored.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - should this callback return a SslStreamCertificateContext instead of an X509Certificate2Collection, since it appears that is the correct API for building a client certificate chain with intermediates

the only problem is that ClientCertificateContext option (which gets assigned) is not available in net6.0

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that API popped up in net8, there is a mega thread about it here: dotnet/runtime#26323


/// <summary>
/// String or file path to PEM-encoded X509 CA Certificate
Expand All @@ -84,7 +85,7 @@ public sealed record NatsTlsOpts
/// <summary>TLS mode to use during connection</summary>
public TlsMode Mode { get; init; }

internal bool HasTlsCerts => CertFile != default || KeyFile != default || LoadClientCert != default || CaFile != default || LoadCaCerts != default;
internal bool HasTlsCerts => CertFile != default || KeyFile != default || LoadClientCerts != default || CaFile != default || LoadCaCerts != default;

internal TlsMode EffectiveMode(NatsUri uri) => Mode switch
{
Expand All @@ -99,21 +100,73 @@ internal bool TryTls(NatsUri uri)
}

/// <summary>
/// Helper method to load a Client Certificate from a pem-encoded string
/// Helper method to load a client certificates and its key from PEM-encoded texts.
/// </summary>
public static Func<ValueTask<X509Certificate2>> LoadClientCertFromPem(string certPem, string keyPem)
/// <param name="certPem">Text of PEM-encoded certificates</param>
/// <param name="keyPem">Text of PEM-encoded key</param>
/// <returns>Returns a callback that will return a collection of certificates</returns>
/// <remarks>
/// Client certificates string may contain multiple certificates, in which case the first
/// certificate is used as the client certificate and the rest are used as intermediates.
/// Using intermediate certificates is only supported on targets .NET 8 and above.
/// </remarks>
public static Func<ValueTask<X509Certificate2Collection>> LoadClientCertsFromPem(string certPem, string keyPem)
{
var clientCert = X509Certificate2.CreateFromPem(certPem, keyPem);
return () => ValueTask.FromResult(clientCert);
var certificateCollection = LoadClientCertsFromMultiPem(certPem, keyPem);

return () => ValueTask.FromResult(certificateCollection);
}

/// <summary>
/// Helper method to load CA Certificates from a pem-encoded string
/// Helper method to load CA certificates from a PEM-encoded text.
/// </summary>
/// <param name="caPem">Text of PEM-encoded CA certificates</param>
/// <returns>Returns a callback that will return a collection of CA certificates</returns>
public static Func<ValueTask<X509Certificate2Collection>> LoadCaCertsFromPem(string caPem)
{
var caCerts = new X509Certificate2Collection();
caCerts.ImportFromPem(caPem);
return () => ValueTask.FromResult(caCerts);
}

/// <summary>
/// Helper method to load a client certificates and its key from PEM-encoded files
/// </summary>
internal static Func<ValueTask<X509Certificate2Collection>> LoadClientCertsFromPemFile(string certPemFile, string keyPemFile)
{
var certPem = File.ReadAllText(certPemFile);
var keyPem = File.ReadAllText(keyPemFile);
var certificateCollection = LoadClientCertsFromMultiPem(certPem, keyPem);

return () => ValueTask.FromResult(certificateCollection);
}

/// <summary>
/// Helper method to load a Client Certificates from a PEM-encoded string
/// </summary>
private static X509Certificate2Collection LoadClientCertsFromMultiPem(ReadOnlySpan<char> certPem, ReadOnlySpan<char> keyPem)
{
var multiPemCertificateCollection = new X509Certificate2Collection();
var addKey = true;

while (PemEncoding.TryFind(certPem, out var fields))
{
X509Certificate2 certificate;

if (addKey)
{
certificate = X509Certificate2.CreateFromPem(certPem, keyPem);
addKey = false;
}
else
{
certificate = X509Certificate2.CreateFromPem(certPem);
}

multiPemCertificateCollection.Add(certificate);
certPem = certPem[fields.Location.End..];
}

return multiPemCertificateCollection;
}
}
1 change: 1 addition & 0 deletions tests/NATS.Client.Core.Tests/NATS.Client.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
</ItemGroup>

<ItemGroup>
Expand Down
49 changes: 44 additions & 5 deletions tests/NATS.Client.Core.Tests/TlsCertsTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;

namespace NATS.Client.Core.Tests;

Expand Down Expand Up @@ -51,7 +53,7 @@ await ValidateAsync(new NatsTlsOpts
});
await ValidateAsync(new NatsTlsOpts
{
LoadClientCert = NatsTlsOpts.LoadClientCertFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile)),
LoadClientCerts = NatsTlsOpts.LoadClientCertsFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile)),
});

return;
Expand All @@ -73,11 +75,48 @@ static async Task ValidateAsync(NatsTlsOpts opts)
}

[Fact]
public async Task Client_connect()
public async Task Load_client_cert_chain_and_key()
{
const string clientCertFile = "resources/certs/chainedclient-cert.pem";
const string clientKeyFile = "resources/certs/chainedclient-key.pem";

await ValidateAsync(new NatsTlsOpts
{
CertFile = clientCertFile,
KeyFile = clientKeyFile,
});
await ValidateAsync(new NatsTlsOpts
{
LoadClientCerts = NatsTlsOpts.LoadClientCertsFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile)),
});

return;

static async Task ValidateAsync(NatsTlsOpts opts)
{
var certs = await TlsCerts.FromNatsTlsOptsAsync(opts);

Assert.NotNull(certs.ClientCerts);
Assert.Equal(3, certs.ClientCerts.Count);

certs.ClientCerts[0].Subject.Should().Be("CN=leafclient");
var encryptValue = certs.ClientCerts[0].GetRSAPublicKey()!.Encrypt(Encoding.UTF8.GetBytes("test123"), RSAEncryptionPadding.OaepSHA1);
var decryptValue = certs.ClientCerts[0].GetRSAPrivateKey()!.Decrypt(encryptValue, RSAEncryptionPadding.OaepSHA1);
Encoding.UTF8.GetString(decryptValue).Should().Be("test123");
certs.ClientCerts[1].Subject.Should().Be("CN=intermediate02");
certs.ClientCerts[2].Subject.Should().Be("CN=intermediate01");
}
}

[SkippableTheory]
[InlineData("resources/certs/client-cert.pem", "resources/certs/client-key.pem", 6)]
[InlineData("resources/certs/chainedclient-cert.pem", "resources/certs/chainedclient-key.pem", 8)]
public async Task Client_connect(string clientCertFile, string clientKeyFile, int minimumFrameworkVersion)
{
var version = int.Parse(Regex.Match(RuntimeInformation.FrameworkDescription, @"(\d+)\.\d").Groups[1].Value);
Skip.IfNot(version >= minimumFrameworkVersion, $"Requires .NET {minimumFrameworkVersion}");

const string caFile = "resources/certs/ca-cert.pem";
const string clientCertFile = "resources/certs/client-cert.pem";
const string clientKeyFile = "resources/certs/client-key.pem";

await using var server = NatsServer.Start(
new NullOutputHelper(),
Expand All @@ -97,7 +136,7 @@ public async Task Client_connect()
await Validate(server, new NatsTlsOpts
{
LoadCaCerts = NatsTlsOpts.LoadCaCertsFromPem(await File.ReadAllTextAsync(caFile)),
LoadClientCert = NatsTlsOpts.LoadClientCertFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile)),
LoadClientCerts = NatsTlsOpts.LoadClientCertsFromPem(await File.ReadAllTextAsync(clientCertFile), await File.ReadAllTextAsync(clientKeyFile)),
});

return;
Expand Down
52 changes: 26 additions & 26 deletions tests/NATS.Client.Core.Tests/resources/certs/ca-cert.pem
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE+zCCAuOgAwIBAgIUTZtqCF7X/ZnbAD3cUyPJ0Xwf+BcwDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEAwwCY2EwHhcNMjMxMTA5MTQyNjMyWhcNMzMxMTA2MTQyNjMy
MIIE+zCCAuOgAwIBAgIUaIS3fVNjOgYm5uFgS/kv6yuIKucwDQYJKoZIhvcNAQEL
BQAwDTELMAkGA1UEAwwCY2EwHhcNMjQwMzI3MTI1NjM3WhcNMzQwMzI1MTI1NjM3
WjANMQswCQYDVQQDDAJjYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AOQsrNyw/kYzNjeUwm2bccCoXW/pyUbB6ak7zD5nxDX5fb+raFyIOJuIsW1aXMEh
613nQhCbWr284xDzU7vInIA6XCgb1BNAsZ3zClw4eHYBWf9B9AIdgRdr5Fdl3SFn
9jnyBG7K2UCYA6Qw0sc+AD2VrYj+wa2LTcfPDaiGrF2XI0o1UmgvNN/NfcZLbFU/
nejbU5uwQ7f3xTjS9nc0kZEJ+Pma19HoIqsvBjaioN4uQvjj1U0GVJ2Z0IwFGm/X
rn5s1MI6QvuJG8igduEUokurOkFqP8i4QNFs0S08bN8ZL8e+1nO/DKsDdq4JICUJ
Mvbu9kmDB/DVZmJIh3vlsWyiOGAovbRqbj2fOUWDE54MFNui0xOp3QJV3ltImu2I
KOVasdqPKPDZjsymIL+K5U9vfwQJkQj1Ww4Y9yF7/aF8/Wm4P255k1Xo6wrFvLRa
G6YNZT66tW8J32C5zOSIBR3huMrRNDUki73CfkXmuTbA9UIJn9kyQvo0nAu8Td8z
lOZBKxE/GMv7JbD0dUMv/NiRpiMI/jatkgKE++1X+sbVvb7AFNsT1Fk9Zle3aK2s
akc43LyZIU+qsb7Feo4O7BqwAinS3nUwUGyThKcTlxIGyr/obV4CWU2GQEbQyUPr
9fz2j1X+IDidnsgtWxYxs4CvA7qLS/zJFdBkxV2iy1VZAgMBAAGjUzBRMB0GA1Ud
DgQWBBR4XdOrzbH3nMaDgT+ROqXFMasyEjAfBgNVHSMEGDAWgBR4XdOrzbH3nMaD
gT+ROqXFMasyEjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQC4
8NMZHrt2od0l6JNU3eg1Kd9A6dAZvc899pIl/DsbmvZk26IglV6s4yxZqIsC6Bw6
CfBaJuPjsJjEHJcBEyRQ7pcIxQsHmHC/bd/58FKf5N2DoaZDjF6U/eGDZj7T9Hkl
cyshAWdkGV58yhlqnrSIG6ovy8wrj6px3cPXG6qM0GopT8GSull9R1kAjVcB9p32
C8rwvmQeRrIUivOduzrhG2kRrqGgPSvkWqTscI9Z7nu5cCIzIcC7QA2UWOixUVDe
ENxhECEvR9BpBZqlZLC0Ihi/zBHlXq+6cAdv61hkAh6mhgaZZjx302gjxGy+c7p8
UBhfGa5dGY+0dJhPWAPAtfX41PQ/Hc85GYkTEVJbP0q60MCegQfvIc4boPG7Mw+Y
FLjhGtBtBF696P6IZpNryV6WjZxi6WWAfqSoTH5GvZR0t1XLvIloizkRODmbcJxA
5my/DBCnUBxrH++yFJs3wd7VCESd+SFJSZQJTDwq/ICrrb71tBqhuUo6uNoEUJ0z
iVikrvjc3JtrnvE+lj56vtzVZ0l6DvNzyihHbPrjq58z3ETeKza/KjZcdLpygIUL
COhwGCxq/fFVJE6wItAVLhYh90yNirIP5b3750jUirjjq75fXfuQbQoE5w+BMByx
PS1vntJkJ6BSgW5vOk9UOGSUYAQAwR28cVD4fL9/GA==
ALV2DvN/xQMBcCpjXe2wXo5khlfbw6CzhsPnieQScDLw1onxuO0+uUN0l4YaMqC4
gn560gct+Hb41wx2j40MU1zm6eYH0CalWuPpPDHx4f/sEVbd9LCHVem+IxrHAnyU
yp6RPQlJvsJr3m617YNIrZDANLrt0Ssk41XdvFsx6tFFTU5SgIYvWOHiIbqkNTAn
6iAdPKBKPM4jNRczocLWP+sbo8TtALMD+0blDjZ9Ue7WqUKnx6ns78oH4XQWyxD0
Bgd6UPGCNbuSJ1BdnE31/M/x8DLOOvkczNCXRp4nCn0VBEvwu1uS77aHN6xrNtN9
nWF/ykTMKlgw8oznSt1znD1QZU/uB2cwomnXGQZ/d4ocrSNW+gNM8JmxHTnO/FRk
UN5KWkRoHOFgzv7e4sW9p7UKzdmYAQRTHn+XgSg7/AFY7J0rQoj5ckrGgJ+MItTY
lVLNuPXPIi7x/bfXKiWDfhrXKMaP08hFB30gTCZ9vpMea7CEOJ6MhJH2yuZnKgjJ
ez65uCu7NMoFrN7y3IcDL/ZrEnaHRJLMBzvGgEoYZ6Ypf7oNJMi8uXnfhqclGpaj
SpLP+31yd2jHYkii2fAZAHzzXse+aT1OvVhShdlcNTSUFwjroMYlaTG92llX1FE4
gn51oj13QGBK9lpfh/uLjfM6+4LDjmQbHDzhTOEQpkqhAgMBAAGjUzBRMB0GA1Ud
DgQWBBTdKnIqsIQL44dltqE1+aFtjASH2TAfBgNVHSMEGDAWgBTdKnIqsIQL44dl
tqE1+aFtjASH2TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAi
+NFuLAn7yjpuBvmncClbMMTuZDo1PH79PIBVESGid2gRye0vXFNqXm20VKEDsM71
6o78bTu+zNBpXyqluqDVjSN+nkeiNytTqsKsUPPMUujtFibc7yC9j2Woj3j+akar
lc5OtDtW/Lkk7Ne9aBQGYa7l0bQuE5OuZquCRpn1HhovNV/otLuaLGJZWoiZ1fry
l0SV4JmEQi1D9T13SeJ2lu/KNHKfs9noCXzE4BjvHFcmk8kxInyfqxbBVJUOzAAl
ELVl83MRSFFX3FvYX3A1OWNY9F6w5TiuYSaGTFqMJwcWXL9KHm4F0gHxyjZCbgeI
O2HJtXq/MfCibgvXV3ghu8v/YPyMTmagAaxeWawMD/QyIJaNVeXdyoG6TnpccB58
F2jiMgobYuN4whj6tpkVXSS6PGobJhl9EaT6hPjfmcIRpTn6j0hVZyKwDOO0cWon
XRY5XNLVpCqQCZNuvzkHc38s5KEuL+BLwPTygVg9OpC8OvbVXS+xhj26edLvwagq
+FxEatM20ytgX1+nl4XKMtcOxqS0eIwlWm0eQS6PjtCLw1GLU/A2cJA27TjjHw6C
EJkaOr5JAv0NSo7pjUW3hu/71frwq2AiCvBPAsKAsPi2pRwHhezyoUNYiaRgzrHX
4IKi1S1XOAy5OVRDZ6SI4iahfOz+/eJtBltk06V3dQ==
-----END CERTIFICATE-----
Loading
Loading