Skip to content

[HTTPS] Adds PEM support for Kestrel #23584

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

Merged
merged 9 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -614,4 +614,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="ArgumentTimeSpanGreaterOrEqual" xml:space="preserve">
<value>A TimeSpan value greater than or equal to {value} is required.</value>
</data>
<data name="InvalidPemKey" xml:space="preserve">
<value>The provided key file is missing or invalid.</value>
</data>
</root>
2 changes: 2 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ public CertificateConfig(IConfigurationSection configSection)

public string Path { get; set; }

public string KeyPath { get; set; }

public string Password { get; set; }

// Cert store
Expand Down
120 changes: 118 additions & 2 deletions src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
Expand Down Expand Up @@ -435,14 +436,129 @@ private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endp
}
else if (certInfo.IsFileCert)
{
var env = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
return new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path), certInfo.Password);
var environment = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
var certificatePath = Path.Combine(environment.ContentRootPath, certInfo.Path);
if (certInfo.KeyPath != null)
{
var certificateKeyPath = Path.Combine(environment.ContentRootPath, certInfo.KeyPath);
X509Certificate2 certificate = GetCertificate(certInfo, certificatePath, certificateKeyPath);

if (!certificate.HasPrivateKey)
{
certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
}

if (certificate != null)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return PersistKey(certificate);
}

return certificate;
}

throw new InvalidOperationException(CoreStrings.InvalidPemKey);
Copy link
Member

Choose a reason for hiding this comment

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

This could throw if the cert is missing, not just because the key was missing or invalid right? This exception message implies the key must be the problem?

And why do we use the exact same exception and log message any time the key is missing or invalid? It would be a lot better to log exactly why the key is invalid and to be very clear when the key is actually missing vs being invalid in both the exception and log messages.

}

return new X509Certificate2(Path.Combine(environment.ContentRootPath, certInfo.Path), certInfo.Password);
}
else if (certInfo.IsStoreCert)
{
return LoadFromStoreCert(certInfo);
}
return null;

static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
{
// We need to force the key to be persisted.
// See https://github.com/dotnet/runtime/issues/23749
var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
}

static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string password)
{
var keyText = File.ReadAllText(keyPath);
return TryAttachPemRSAKey(certificate, keyText, password) ??
TryAttachPemDSAKey(certificate, keyText, password) ??
TryAttachPemECDSAKey(certificate, keyText, password);
}

static X509Certificate2 GetCertificate(CertificateConfig certInfo, string certificatePath, string certificateKeyPath)
{
if (X509Certificate2.GetCertContentType(certificatePath) != X509ContentType.Unknown)
{
return new X509Certificate2(certificatePath);
}

return certInfo.Password != null ?
X509Certificate2.CreateFromEncryptedPemFile(certificatePath, certInfo.Password, certificateKeyPath) :
X509Certificate2.CreateFromPemFile(certificatePath, certificateKeyPath);
}
}

private static X509Certificate2 TryAttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
{
const string RSAOid = "1.2.840.113549.1.1.1";
if (string.Equals(RSAOid, certificate.PublicKey.Oid.Value, StringComparison.Ordinal))
{
using var rsa = RSA.Create();
if (password == null)
{
rsa.ImportFromPem(keyText);
}
else
{
rsa.ImportFromEncryptedPem(keyText, password);
}

return certificate.CopyWithPrivateKey(rsa);
}

return null;
}

private static X509Certificate2 TryAttachPemDSAKey(X509Certificate2 certificate, string keyText, string password)
{
const string DSAOid = "1.2.840.10040.4.1";
if (string.Equals(DSAOid, certificate.PublicKey.Oid.Value, StringComparison.Ordinal))
{
using var dsa = DSA.Create();
if (password == null)
{
dsa.ImportFromPem(keyText);
}
else
{
dsa.ImportFromEncryptedPem(keyText, password);
}

return certificate.CopyWithPrivateKey(dsa);
}

return null;
}

private static X509Certificate2 TryAttachPemECDSAKey(X509Certificate2 certificate, string keyText, string password)
{
const string ECDsaOid = "1.2.840.10045.2.1";
if (string.Equals(ECDsaOid, certificate.PublicKey.Oid.Value, StringComparison.Ordinal))
{
using var ecdsa = ECDsa.Create();
if (password == null)
{
ecdsa.ImportFromPem(keyText);
}
else
{
ecdsa.ImportFromEncryptedPem(keyText, password);
}

return certificate.CopyWithPrivateKey(ecdsa);
}

return null;
}

private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<Compile Include="$(KestrelSharedSourceRoot)test\*.cs" LinkBase="shared" />
<Compile Include="$(KestrelSharedSourceRoot)KnownHeaders.cs" LinkBase="shared" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.crt" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.key" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\**\*.cs" Link="Shared\runtime\%(Filename)%(Extension)" />
Expand Down
124 changes: 123 additions & 1 deletion src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.AspNetCore.Hosting;
Expand All @@ -25,7 +26,7 @@ public class KestrelConfigurationLoaderTests
private KestrelServerOptions CreateServerOptions()
{
var serverOptions = new KestrelServerOptions();
var env = new MockHostingEnvironment { ApplicationName = "TestApplication" };
var env = new MockHostingEnvironment { ApplicationName = "TestApplication", ContentRootPath = Directory.GetCurrentDirectory() };
serverOptions.ApplicationServices = new ServiceCollection()
.AddLogging()
.AddSingleton<IHostEnvironment>(env)
Expand Down Expand Up @@ -254,6 +255,127 @@ public void ConfigureEndpointDevelopmentCertificateGetsLoadedWhenPresent()
}
}

[Fact]
public void ConfigureEndpoint_CanLoadRsaPemCerts()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-rsa.crt"));

var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-rsa.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-rsa.key")),
}).Build();

serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
}).Load();

Assert.True(ran1);
Assert.NotNull(serverOptions.DefaultCertificate);
}

[Fact]
public void ConfigureEndpoint_CanLoadProtectedRsaPemCerts()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));

var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key")),
new KeyValuePair<string, string>("Certificates:Default:Password", "aspnetcore"),
}).Build();

serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
}).Load();

Assert.True(ran1);
Assert.NotNull(serverOptions.DefaultCertificate);
}

[Fact]
public void ConfigureEndpoint_ThrowsWhen_TheKeyCannotBeRead()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));

var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key"))
}).Build();

var ex = Assert.Throws<InvalidOperationException>(() =>
{
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
}).Load();
});
Assert.Equal(CoreStrings.InvalidPemKey, ex.Message);
}

[Theory]
[InlineData("https-rsa.pem", "https-rsa.key", null)]
[InlineData("https-rsa.pem", "https-rsa-protected.key", "aspnetcore")]
[InlineData("https-rsa.crt", "https-rsa.key", null)]
[InlineData("https-rsa.crt", "https-rsa-protected.key", "aspnetcore")]
[InlineData("https-ecdsa.pem", "https-ecdsa.key", null)]
[InlineData("https-ecdsa.pem", "https-ecdsa-protected.key", "aspnetcore")]
[InlineData("https-ecdsa.crt", "https-ecdsa.key", null)]
[InlineData("https-ecdsa.crt", "https-ecdsa-protected.key", "aspnetcore")]
//[InlineData("https-dsa.pem", "https-dsa.key", null)]
//[InlineData("https-dsa.pem", "https-dsa-protected.key", "aspnetcore")]
//[InlineData("https-dsa.crt", "https-dsa.key", null)]
//[InlineData("https-dsa.crt", "https-dsa-protected.key", "aspnetcore")]
public void ConfigureEndpoint_CanLoadPemCertificates(string certificateFile, string certificateKey, string password)
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath(Path.ChangeExtension(certificateFile, "crt")));

var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", certificateFile)),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", certificateKey)),
}
.Concat(password != null ? new[] { new KeyValuePair<string, string>("Certificates:Default:Password", password) } : Array.Empty<KeyValuePair<string, string>>()))
.Build();

serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
}).Load();

Assert.True(ran1);
Assert.NotNull(serverOptions.DefaultCertificate);
}

[Fact]
public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPasswordIsNotCorrect()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<Compile Include="$(SharedSourceRoot)NullScope.cs" />
<Compile Include="$(KestrelSharedSourceRoot)test\*.cs" LinkBase="shared" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.crt" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.key" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pem" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelRoot)Core\src\Internal\Http\HttpHeaders.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelRoot)Core\src\Internal\Http\HttpProtocol.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelRoot)Core\src\Internal\Infrastructure\HttpUtilities.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.key binary
*.pem binary
Binary file not shown.
30 changes: 30 additions & 0 deletions src/Servers/Kestrel/shared/test/TestCertificates/https-aspnet.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFNjBgBgkqhkiG9w0BBQ0wUzAyBgkqhkiG9w0BBQwwJQQQ93oRxzJ5UoNOb/zN
x5cdsAIDAYagMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAuHsE18X/Z9ZVe
aBl7C55nBIIE0AABqjc9ERcLYNpCRpA6c/TFG62m4Mr9J4dU4g1WD07t7uLxZiRi
Pl0YOjCulljMsevAW5PlLxi4ffJ+I0/UB1WOfzEMhcj7o1qG0Uv55B7WRuWKw1Zr
jo0bDY5Man48ZjpqMMBdnWhyHdIDm+WD0OyN98mpsN6SCQMjvx91M+klrsp7LOMM
88HHS0RFVKGF9hYSy6rCwMJWf+7QGO2wXfq+MKvJ/bBgPGDLwN4phUCyocnR0swD
/XZNiiw0xIC8OxAKhc6BV4AJkjNs32THdBOCGY6B4P/9Zo5W29S3ja/hGsMQAA27
QtIDg74HpX7TgIyqoc1oiLNIWW/+jUHSEYJsTPlg5VYWsXUfSHZpz8EJvKt2tyvt
vBGOCLDDZD4GVXhPigKG6zJSJeTe94/VlwPhNSEucKeaALdax5t3HvPNzWKFX57E
aC82/IxRrjgHmgsGSZdMi08HY6K9GAVBFpIGvXOGtRq7w8zO/KagAvSwAOLLtOs7
iEuAQxD+cKLRT59c4E7r5W7BT+faq85ovqdXe5Edtl3cT81zsl27pZvQrcrTPbZe
4OeIdWxOmOnC/bXvRHNd9XuYadXXazBoFbe9yPwjqnflEh39CyvlOZXeaQXSdsEM
1IBhddRTorO/I8M/znu9glqIa5ya1NA+4ujmf4OnJLtsrlKQa65VPVTrFdeYuMr0
VfOuuIye2OdyJ6jS0a1PYQm4bEEz6UR88dnmnhDx6i8/l2wW5+CArA/x8IBYboBM
NJpJY9bHpic1AhjnjnTtFz2s4uYPi5g9peBizarZn+6OJvgYqs4a8SI92dA3E2o4
a/1j7xlLlgXnVRLBMibxqzjMt4Zt7Nj+BaN1owrB/q04AWS2M4TSQz+NYOZwNFxB
dzb+fysTLK5XNEYq6rSg+0i+EKZl8Jb/t4d8SLPVr/tdfDt9BtZ0nTgjvy1HWy1p
kQdm13XfK1/9KsePH/Jb6dvN/u6ubV+ZqI7Bc7VyTi0bKMdpH2K8/dtopNyDZ/P+
/IsyyDYTorgJB/klSih/W0hqpSBbEAmlSBfBxP1/ozBEGR2oF20JOCFyD6UXQR/1
V7r2KtplpyfXaIWh4fABitAMHz7VgmEIQ2H9cB4Ey9jdRPQ/1p+OgGjfaFJQ0uYM
987TDtjkuukJYnPZNIIx0Yv3iAX16XmhzJixWSMUIJiWfSiz0aTjBxsPQVPTQV+M
6BgFf3riBApZYlVVJsGIie2XTvu/tHRhfQrxccl63HN7yAeJheQnoscin6Z5TKN/
U8Ouy/QGiATatKUEUjr4lN+BYySf8F6e3cAAeAx/ZnFvGw5z8fwNYBjVWg/83bTw
9rS+tSk8VsvTdkcKoNbbDtw+SwYfZSbMUBFm0B13190iJZoyWI+5ZKPnZ2CvOZhX
PjGTOnh6Diq907l2Q7S/v8SLe0bCHCHVBy+CcPWVDZ6Z7V5cJ/W8TvFPcSGw1UCl
tKPp862uDaPKvGxqGDq0vGouEUrtJKZ279Lnrtz1n8raUj0Gxa+KXqLACh8dXCzK
ZgCTPhfAjZcYgA73edW0whNNH9MNInDGulT/arCK3HTkFPczD+7wA8Ojw/LxKFJs
0d8vtILbmLv46CO+wvIdWrW1c7PCrGJDf9Zuw06vIH7hpW9swSM55k9/
-----END ENCRYPTED PRIVATE KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICzTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIgeRX7Sed0OsCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBC7vpWM+TQ9caiqYAn+h95YBIIC
cM8+gZPkVJZF1ICFi6lHHNjV9FSdH+qwvUOzBEVP1oZtmdMBhEVqqm+3H+Gcm6dn
C4JjMGCu3KqQ096JoWeFE/ksuTcL7MBtvyY35NIbbVxEW7YxvTiICADfjwL0mDYH
SlUr0LLrMhoYfN01HE2kkkJLZLt58zjzxyUdc//SlIC6rJyd6NZdNrmRQ9JZzd7G
SZtkXRMvrzyJoxwOc+5ERmYtOtOWmyF2pAFwoEnb8VOQgoM3gy/3lO5aVpPj76Rr
MmJwdruU39nUPTe6VI6ukF5T27pu0XmX5YWqy5+PZ5cFhmOxGSxwvw6dbJSM1+Sg
ZcKohhQQtNFYFgFpp8cz84o1lHWprjGxHRVKEeKAwgX8v8glDTKzwS0DKsTzdn0s
2RCwjr+GxH7wpjwzllny2xU/h5ZR2j8gldfCNFVPtFrGleuEduPA4U04Mkije0l9
AdPHDK8drvUuIZLb2HCQu0wYmUDmtY/SWuMl1xRonrF+cPPcmAdQDP8IH2MGL8SO
9h/CCextHsqzXl7PJzHcjLmn8qmlocmSJRALCrWpgoE/d7Wjle8LmtQVWSw0UgTo
GEoWpFcz6JJBUrcasGQHokLtuyAjjSRgSt1DFK2iW+O7MARK9wJNCGUfDNWavO4p
hA7MR1muv/xwwr0oi2OJ5rc3N+6M/Fb1fRlU5+EsaE2xee7nV3JdVmdYKpOM8ksX
F5XV/b4BbaPbTRppr0aZk6RDqy5jlYVsNy1Npwqw/2pJqS7wTWZgaWQgePsRGsb5
3SZC64XpHlzBgYOgpnOC9x9KJuWt2z4OS0l70Nvv7+VLzem0fVMb+Dh/5VFjYfp2
4w==
-----END ENCRYPTED PRIVATE KEY-----
Binary file not shown.
15 changes: 15 additions & 0 deletions src/Servers/Kestrel/shared/test/TestCertificates/https-dsa.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN PRIVATE KEY-----
MIICZQIBADCCAjkGByqGSM44BAEwggIsAoIBAQDJCLNPr0jF6+UOqnftcW7OC+Pj
rSOLhk277kKiWlQvOGNyqFbL3UvOfVAbKFcMOumZhftsSvHUf9Q47xS5XKBbhIpz
UhyxLVQ435mqQWYOpnqLm5glxUwUn8DKfFiOTznimdypgnGQt5mTdVJ7IG574fAn
5IdC6oE1KReDxdXqN0M4i2e6x/O+uc7r8ePsuQ/Eoo3w4iM/VxHKVLNoOBWZwgPh
EpxumDFslRGal2j7IlUit1tEC9k9D32G+VL7gqu0m6LO5JXJNFIcU6RH36YDfLd3
eTKbwZjQW57uj1JkR/flm0xR7IUVKmcVs0QaW5fyeudt+U78fCGfd8CSa2cDAiEA
vDbQX231dqIHsQePToTsintUmbtI+/MqrR27x0fHysMCggEAHCTRNw9ZVDUcUtD0
LKL1ophDNXZFjegz6DwHEsGwMcW+fT9QwfVmcZy5xLqqloapOdE7ejWJ6rKHPmQz
m+73u7fuDDSJMl7MaxVDSMLmlCnJO84tNtCLyRsYxsneSFs9rw0tEIJGAgJHnAgz
usj/hNEhTZT5oLjVPT/DjEqqOvVTlBaVZnd1fkeuI03M/J86R2mIm0VkFLpufnW/
McebsHwpz9Y2jxr2Wvz0+xv5ooLwZeVFRVl95SsHypjkCGtWKcqxA+lZdYkqLbwt
WvIfo+gJJS6iLsxs9I6OBqh85+bp4xlYsXnmr3JrsuV7OyfCppfgIABbHops57Vg
rbRwswQjAiEAksfTXws/dqMjlRmytRcU4qDef3La1STF5WSsoMdYYqs=
-----END PRIVATE KEY-----
Loading