Skip to content

Commit 9d3bf57

Browse files
authored
[HTTPS] Adds PEM support for Kestrel (#23584)
* Adds support for loading PEM certificates and keys in Kestrel. * You can load PEM Certificate + PKCS8 encoded PEM Keys. * Certificates in DER format + PKCS8 encoded PEM Keys. * Supported key types are: * RSA * ECSA * DSA
1 parent dc477ed commit 9d3bf57

24 files changed

+480
-3
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,4 +614,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
614614
<data name="ArgumentTimeSpanGreaterOrEqual" xml:space="preserve">
615615
<value>A TimeSpan value greater than or equal to {value} is required.</value>
616616
</data>
617+
<data name="InvalidPemKey" xml:space="preserve">
618+
<value>The provided key file is missing or invalid.</value>
619+
</data>
620+
<data name="UnrecognizedCertificateKeyOid" xml:space="preserve">
621+
<value>Unknown algorithm for certificate with public key type '{0}'.</value>
622+
</data>
617623
</root>

src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ public CertificateConfig(IConfigurationSection configSection)
205205

206206
public string Path { get; set; }
207207

208+
public string KeyPath { get; set; }
209+
208210
public string Password { get; set; }
209211

210212
// Cert store

src/Servers/Kestrel/Core/src/Internal/LoggerExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ internal static class LoggerExtensions
4646
new EventId(5, "DeveloperCertificateFirstRun"),
4747
"{Message}");
4848

49+
private static readonly Action<ILogger, string, Exception> _failedToLoadCertificate =
50+
LoggerMessage.Define<string>(
51+
LogLevel.Error,
52+
new EventId(6, "MissingOrInvalidCertificateFile"),
53+
"The certificate file at '{CertificateFilePath}' can not be found, contains malformed data or does not contain a certificate.");
54+
55+
private static readonly Action<ILogger, string, Exception> _failedToLoadCertificateKey =
56+
LoggerMessage.Define<string>(
57+
LogLevel.Error,
58+
new EventId(7, "MissingOrInvalidCertificateKeyFile"),
59+
"The certificate key file at '{CertificateKeyFilePath}' can not be found, contains malformed data or does not contain a PEM encoded key in PKCS8 format.");
60+
4961
public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
5062

5163
public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null);
@@ -57,5 +69,8 @@ internal static class LoggerExtensions
5769
public static void BadDeveloperCertificateState(this ILogger logger) => _badDeveloperCertificateState(logger, null);
5870

5971
public static void DeveloperCertificateFirstRun(this ILogger logger, string message) => _developerCertificateFirstRun(logger, message, null);
72+
73+
public static void FailedToLoadCertificate(this ILogger logger, string certificatePath) => _failedToLoadCertificate(logger, certificatePath, null);
74+
public static void FailedToLoadCertificateKey(this ILogger logger, string certificateKeyPath) => _failedToLoadCertificateKey(logger, certificateKeyPath, null);
6075
}
6176
}

src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Net;
9+
using System.Runtime.InteropServices;
910
using System.Security.Authentication;
1011
using System.Security.Cryptography;
1112
using System.Security.Cryptography.X509Certificates;
@@ -429,20 +430,133 @@ private bool TryGetCertificatePath(out string path)
429430

430431
private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
431432
{
433+
var logger = Options.ApplicationServices.GetRequiredService<ILogger<KestrelConfigurationLoader>>();
432434
if (certInfo.IsFileCert && certInfo.IsStoreCert)
433435
{
434436
throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
435437
}
436438
else if (certInfo.IsFileCert)
437439
{
438-
var env = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
439-
return new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path), certInfo.Password);
440+
var environment = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
441+
var certificatePath = Path.Combine(environment.ContentRootPath, certInfo.Path);
442+
if (certInfo.KeyPath != null)
443+
{
444+
var certificateKeyPath = Path.Combine(environment.ContentRootPath, certInfo.KeyPath);
445+
var certificate = GetCertificate(certificatePath);
446+
447+
if (certificate != null)
448+
{
449+
certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
450+
}
451+
else
452+
{
453+
logger.FailedToLoadCertificate(certificateKeyPath);
454+
}
455+
456+
if (certificate != null)
457+
{
458+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
459+
{
460+
return PersistKey(certificate);
461+
}
462+
463+
return certificate;
464+
}
465+
else
466+
{
467+
logger.FailedToLoadCertificateKey(certificateKeyPath);
468+
}
469+
470+
throw new InvalidOperationException(CoreStrings.InvalidPemKey);
471+
}
472+
473+
return new X509Certificate2(Path.Combine(environment.ContentRootPath, certInfo.Path), certInfo.Password);
440474
}
441475
else if (certInfo.IsStoreCert)
442476
{
443477
return LoadFromStoreCert(certInfo);
444478
}
445479
return null;
480+
481+
static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
482+
{
483+
// We need to force the key to be persisted.
484+
// See https://github.com/dotnet/runtime/issues/23749
485+
var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
486+
return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
487+
}
488+
489+
static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string password)
490+
{
491+
// OIDs for the certificate key types.
492+
const string RSAOid = "1.2.840.113549.1.1.1";
493+
const string DSAOid = "1.2.840.10040.4.1";
494+
const string ECDsaOid = "1.2.840.10045.2.1";
495+
496+
var keyText = File.ReadAllText(keyPath);
497+
return certificate.PublicKey.Oid.Value switch
498+
{
499+
RSAOid => AttachPemRSAKey(certificate, keyText, password),
500+
ECDsaOid => AttachPemECDSAKey(certificate, keyText, password),
501+
DSAOid => AttachPemDSAKey(certificate, keyText, password),
502+
_ => throw new InvalidOperationException(string.Format(CoreStrings.UnrecognizedCertificateKeyOid, certificate.PublicKey.Oid.Value))
503+
};
504+
}
505+
506+
static X509Certificate2 GetCertificate(string certificatePath)
507+
{
508+
if (X509Certificate2.GetCertContentType(certificatePath) == X509ContentType.Cert)
509+
{
510+
return new X509Certificate2(certificatePath);
511+
}
512+
513+
return null;
514+
}
515+
}
516+
517+
private static X509Certificate2 AttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
518+
{
519+
using var rsa = RSA.Create();
520+
if (password == null)
521+
{
522+
rsa.ImportFromPem(keyText);
523+
}
524+
else
525+
{
526+
rsa.ImportFromEncryptedPem(keyText, password);
527+
}
528+
529+
return certificate.CopyWithPrivateKey(rsa);
530+
}
531+
532+
private static X509Certificate2 AttachPemDSAKey(X509Certificate2 certificate, string keyText, string password)
533+
{
534+
using var dsa = DSA.Create();
535+
if (password == null)
536+
{
537+
dsa.ImportFromPem(keyText);
538+
}
539+
else
540+
{
541+
dsa.ImportFromEncryptedPem(keyText, password);
542+
}
543+
544+
return certificate.CopyWithPrivateKey(dsa);
545+
}
546+
547+
private static X509Certificate2 AttachPemECDSAKey(X509Certificate2 certificate, string keyText, string password)
548+
{
549+
using var ecdsa = ECDsa.Create();
550+
if (password == null)
551+
{
552+
ecdsa.ImportFromPem(keyText);
553+
}
554+
else
555+
{
556+
ecdsa.ImportFromEncryptedPem(keyText, password);
557+
}
558+
559+
return certificate.CopyWithPrivateKey(ecdsa);
446560
}
447561

448562
private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)

src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<Compile Include="$(KestrelSharedSourceRoot)test\*.cs" LinkBase="shared" />
1414
<Compile Include="$(KestrelSharedSourceRoot)KnownHeaders.cs" LinkBase="shared" />
1515
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
16+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.crt" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
17+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.key" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
1618
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
1719
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
1820
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\**\*.cs" Link="Shared\runtime\%(Filename)%(Extension)" />

src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Security.Authentication;
9+
using System.Security.Cryptography;
910
using System.Security.Cryptography.X509Certificates;
1011
using System.Text;
1112
using Microsoft.AspNetCore.Hosting;
@@ -25,7 +26,7 @@ public class KestrelConfigurationLoaderTests
2526
private KestrelServerOptions CreateServerOptions()
2627
{
2728
var serverOptions = new KestrelServerOptions();
28-
var env = new MockHostingEnvironment { ApplicationName = "TestApplication" };
29+
var env = new MockHostingEnvironment { ApplicationName = "TestApplication", ContentRootPath = Directory.GetCurrentDirectory() };
2930
serverOptions.ApplicationServices = new ServiceCollection()
3031
.AddLogging()
3132
.AddSingleton<IHostEnvironment>(env)
@@ -254,6 +255,121 @@ public void ConfigureEndpointDevelopmentCertificateGetsLoadedWhenPresent()
254255
}
255256
}
256257

258+
[Fact]
259+
public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsMissing()
260+
{
261+
var serverOptions = CreateServerOptions();
262+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
263+
264+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
265+
{
266+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
267+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
268+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key"))
269+
}).Build();
270+
271+
var ex = Assert.Throws<ArgumentException>(() =>
272+
{
273+
serverOptions
274+
.Configure(config)
275+
.Endpoint("End1", opt =>
276+
{
277+
Assert.True(opt.IsHttps);
278+
}).Load();
279+
});
280+
}
281+
282+
[Fact]
283+
public void ConfigureEndpoint_ThrowsWhen_TheKeyDoesntMatchTheCertificateKey()
284+
{
285+
var serverOptions = CreateServerOptions();
286+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
287+
288+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
289+
{
290+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
291+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
292+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-ecdsa.key")),
293+
new KeyValuePair<string, string>("Certificates:Default:Password", "aspnetcore")
294+
}).Build();
295+
296+
var ex = Assert.Throws<ArgumentException>(() =>
297+
{
298+
serverOptions
299+
.Configure(config)
300+
.Endpoint("End1", opt =>
301+
{
302+
Assert.True(opt.IsHttps);
303+
}).Load();
304+
});
305+
}
306+
307+
[Fact]
308+
public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsIncorrect()
309+
{
310+
var serverOptions = CreateServerOptions();
311+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
312+
313+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
314+
{
315+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
316+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
317+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key")),
318+
new KeyValuePair<string, string>("Certificates:Default:Password", "abcde"),
319+
}).Build();
320+
321+
var ex = Assert.Throws<CryptographicException>(() =>
322+
{
323+
serverOptions
324+
.Configure(config)
325+
.Endpoint("End1", opt =>
326+
{
327+
Assert.True(opt.IsHttps);
328+
}).Load();
329+
});
330+
}
331+
332+
[Theory]
333+
[InlineData("https-rsa.pem", "https-rsa.key", null)]
334+
[InlineData("https-rsa.pem", "https-rsa-protected.key", "aspnetcore")]
335+
[InlineData("https-rsa.crt", "https-rsa.key", null)]
336+
[InlineData("https-rsa.crt", "https-rsa-protected.key", "aspnetcore")]
337+
[InlineData("https-ecdsa.pem", "https-ecdsa.key", null)]
338+
[InlineData("https-ecdsa.pem", "https-ecdsa-protected.key", "aspnetcore")]
339+
[InlineData("https-ecdsa.crt", "https-ecdsa.key", null)]
340+
[InlineData("https-ecdsa.crt", "https-ecdsa-protected.key", "aspnetcore")]
341+
[InlineData("https-dsa.pem", "https-dsa.key", null)]
342+
[InlineData("https-dsa.pem", "https-dsa-protected.key", "test")]
343+
[InlineData("https-dsa.crt", "https-dsa.key", null)]
344+
[InlineData("https-dsa.crt", "https-dsa-protected.key", "test")]
345+
public void ConfigureEndpoint_CanLoadPemCertificates(string certificateFile, string certificateKey, string password)
346+
{
347+
var serverOptions = CreateServerOptions();
348+
var certificate = new X509Certificate2(TestResources.GetCertPath(Path.ChangeExtension(certificateFile, "crt")));
349+
350+
var ran1 = false;
351+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
352+
{
353+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
354+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", certificateFile)),
355+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", certificateKey)),
356+
}
357+
.Concat(password != null ? new[] { new KeyValuePair<string, string>("Certificates:Default:Password", password) } : Array.Empty<KeyValuePair<string, string>>()))
358+
.Build();
359+
360+
serverOptions
361+
.Configure(config)
362+
.Endpoint("End1", opt =>
363+
{
364+
ran1 = true;
365+
Assert.True(opt.IsHttps);
366+
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
367+
}).Load();
368+
369+
Assert.True(ran1);
370+
Assert.NotNull(serverOptions.DefaultCertificate);
371+
}
372+
257373
[Fact]
258374
public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPasswordIsNotCorrect()
259375
{

src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
<Compile Include="$(SharedSourceRoot)NullScope.cs" />
99
<Compile Include="$(KestrelSharedSourceRoot)test\*.cs" LinkBase="shared" />
1010
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
11+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.crt" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
12+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.key" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
13+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pem" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
1114
<Content Include="$(KestrelRoot)Core\src\Internal\Http\HttpHeaders.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
1215
<Content Include="$(KestrelRoot)Core\src\Internal\Http\HttpProtocol.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
1316
<Content Include="$(KestrelRoot)Core\src\Internal\Infrastructure\HttpUtilities.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.key binary
2+
*.pem binary
Binary file not shown.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIFNjBgBgkqhkiG9w0BBQ0wUzAyBgkqhkiG9w0BBQwwJQQQ93oRxzJ5UoNOb/zN
3+
x5cdsAIDAYagMAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAuHsE18X/Z9ZVe
4+
aBl7C55nBIIE0AABqjc9ERcLYNpCRpA6c/TFG62m4Mr9J4dU4g1WD07t7uLxZiRi
5+
Pl0YOjCulljMsevAW5PlLxi4ffJ+I0/UB1WOfzEMhcj7o1qG0Uv55B7WRuWKw1Zr
6+
jo0bDY5Man48ZjpqMMBdnWhyHdIDm+WD0OyN98mpsN6SCQMjvx91M+klrsp7LOMM
7+
88HHS0RFVKGF9hYSy6rCwMJWf+7QGO2wXfq+MKvJ/bBgPGDLwN4phUCyocnR0swD
8+
/XZNiiw0xIC8OxAKhc6BV4AJkjNs32THdBOCGY6B4P/9Zo5W29S3ja/hGsMQAA27
9+
QtIDg74HpX7TgIyqoc1oiLNIWW/+jUHSEYJsTPlg5VYWsXUfSHZpz8EJvKt2tyvt
10+
vBGOCLDDZD4GVXhPigKG6zJSJeTe94/VlwPhNSEucKeaALdax5t3HvPNzWKFX57E
11+
aC82/IxRrjgHmgsGSZdMi08HY6K9GAVBFpIGvXOGtRq7w8zO/KagAvSwAOLLtOs7
12+
iEuAQxD+cKLRT59c4E7r5W7BT+faq85ovqdXe5Edtl3cT81zsl27pZvQrcrTPbZe
13+
4OeIdWxOmOnC/bXvRHNd9XuYadXXazBoFbe9yPwjqnflEh39CyvlOZXeaQXSdsEM
14+
1IBhddRTorO/I8M/znu9glqIa5ya1NA+4ujmf4OnJLtsrlKQa65VPVTrFdeYuMr0
15+
VfOuuIye2OdyJ6jS0a1PYQm4bEEz6UR88dnmnhDx6i8/l2wW5+CArA/x8IBYboBM
16+
NJpJY9bHpic1AhjnjnTtFz2s4uYPi5g9peBizarZn+6OJvgYqs4a8SI92dA3E2o4
17+
a/1j7xlLlgXnVRLBMibxqzjMt4Zt7Nj+BaN1owrB/q04AWS2M4TSQz+NYOZwNFxB
18+
dzb+fysTLK5XNEYq6rSg+0i+EKZl8Jb/t4d8SLPVr/tdfDt9BtZ0nTgjvy1HWy1p
19+
kQdm13XfK1/9KsePH/Jb6dvN/u6ubV+ZqI7Bc7VyTi0bKMdpH2K8/dtopNyDZ/P+
20+
/IsyyDYTorgJB/klSih/W0hqpSBbEAmlSBfBxP1/ozBEGR2oF20JOCFyD6UXQR/1
21+
V7r2KtplpyfXaIWh4fABitAMHz7VgmEIQ2H9cB4Ey9jdRPQ/1p+OgGjfaFJQ0uYM
22+
987TDtjkuukJYnPZNIIx0Yv3iAX16XmhzJixWSMUIJiWfSiz0aTjBxsPQVPTQV+M
23+
6BgFf3riBApZYlVVJsGIie2XTvu/tHRhfQrxccl63HN7yAeJheQnoscin6Z5TKN/
24+
U8Ouy/QGiATatKUEUjr4lN+BYySf8F6e3cAAeAx/ZnFvGw5z8fwNYBjVWg/83bTw
25+
9rS+tSk8VsvTdkcKoNbbDtw+SwYfZSbMUBFm0B13190iJZoyWI+5ZKPnZ2CvOZhX
26+
PjGTOnh6Diq907l2Q7S/v8SLe0bCHCHVBy+CcPWVDZ6Z7V5cJ/W8TvFPcSGw1UCl
27+
tKPp862uDaPKvGxqGDq0vGouEUrtJKZ279Lnrtz1n8raUj0Gxa+KXqLACh8dXCzK
28+
ZgCTPhfAjZcYgA73edW0whNNH9MNInDGulT/arCK3HTkFPczD+7wA8Ojw/LxKFJs
29+
0d8vtILbmLv46CO+wvIdWrW1c7PCrGJDf9Zuw06vIH7hpW9swSM55k9/
30+
-----END ENCRYPTED PRIVATE KEY-----

0 commit comments

Comments
 (0)