Skip to content

Commit fbe5489

Browse files
committed
Add PEM support
1 parent 4eca8a4 commit fbe5489

6 files changed

+208
-1
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,4 +614,7 @@ 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>
617620
</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/KestrelConfigurationLoader.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,23 @@ private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endp
436436
else if (certInfo.IsFileCert)
437437
{
438438
var env = Options.ApplicationServices.GetRequiredService<IHostEnvironment>();
439+
if (certInfo.KeyPath != null)
440+
{
441+
if (TryReadPemRSAKey(certInfo, out var rsaKey))
442+
{
443+
var publicCertificate = new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path));
444+
return publicCertificate.CopyWithPrivateKey(rsaKey);
445+
}
446+
447+
if (TryReadPemDSAKey(certInfo, out var dsaKey))
448+
{
449+
var publicCertificate = new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path));
450+
return publicCertificate.CopyWithPrivateKey(dsaKey);
451+
}
452+
453+
throw new InvalidOperationException(CoreStrings.InvalidPemKey);
454+
}
455+
439456
return new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path), certInfo.Password);
440457
}
441458
else if (certInfo.IsStoreCert)
@@ -445,6 +462,52 @@ private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endp
445462
return null;
446463
}
447464

465+
private static bool TryReadPemRSAKey(CertificateConfig certInfo, out RSA key)
466+
{
467+
try
468+
{
469+
var rsaKey = RSA.Create();
470+
if (certInfo.Password == null)
471+
{
472+
rsaKey.ImportFromPem(File.ReadAllText(certInfo.KeyPath));
473+
}
474+
else
475+
{
476+
rsaKey.ImportFromEncryptedPem(File.ReadAllText(certInfo.KeyPath), certInfo.Password);
477+
}
478+
key = rsaKey;
479+
return true;
480+
}
481+
catch
482+
{
483+
key = null;
484+
return false;
485+
}
486+
}
487+
488+
private static bool TryReadPemDSAKey(CertificateConfig certInfo, out DSA dsaKey)
489+
{
490+
try
491+
{
492+
var dsa = DSA.Create();
493+
if (certInfo.Password == null)
494+
{
495+
dsa.ImportFromPem(File.ReadAllText(certInfo.KeyPath));
496+
}
497+
else
498+
{
499+
dsa.ImportFromEncryptedPem(File.ReadAllText(certInfo.KeyPath), certInfo.Password);
500+
}
501+
dsaKey = dsa;
502+
return true;
503+
}
504+
catch
505+
{
506+
dsaKey = null;
507+
return false;
508+
}
509+
}
510+
448511
private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
449512
{
450513
var subject = certInfo.Subject;

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: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class KestrelConfigurationLoaderTests
2525
private KestrelServerOptions CreateServerOptions()
2626
{
2727
var serverOptions = new KestrelServerOptions();
28-
var env = new MockHostingEnvironment { ApplicationName = "TestApplication" };
28+
var env = new MockHostingEnvironment { ApplicationName = "TestApplication", ContentRootPath = Directory.GetCurrentDirectory() };
2929
serverOptions.ApplicationServices = new ServiceCollection()
3030
.AddLogging()
3131
.AddSingleton<IHostEnvironment>(env)
@@ -254,6 +254,141 @@ public void ConfigureEndpointDevelopmentCertificateGetsLoadedWhenPresent()
254254
}
255255
}
256256

257+
[Fact]
258+
public void ConfigureEndpoint_CanLoadRsaPemCerts()
259+
{
260+
var serverOptions = CreateServerOptions();
261+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-rsa.crt"));
262+
263+
var ran1 = false;
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-rsa.crt")),
268+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-rsa.key")),
269+
}).Build();
270+
271+
serverOptions
272+
.Configure(config)
273+
.Endpoint("End1", opt =>
274+
{
275+
ran1 = true;
276+
Assert.True(opt.IsHttps);
277+
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
278+
}).Load();
279+
280+
Assert.True(ran1);
281+
Assert.NotNull(serverOptions.DefaultCertificate);
282+
}
283+
284+
[Fact]
285+
public void ConfigureEndpoint_CanLoadProtectedRsaPemCerts()
286+
{
287+
var serverOptions = CreateServerOptions();
288+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
289+
290+
var ran1 = false;
291+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
292+
{
293+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
294+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
295+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key")),
296+
new KeyValuePair<string, string>("Certificates:Default:Password", "aspnetcore"),
297+
}).Build();
298+
299+
serverOptions
300+
.Configure(config)
301+
.Endpoint("End1", opt =>
302+
{
303+
ran1 = true;
304+
Assert.True(opt.IsHttps);
305+
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
306+
}).Load();
307+
308+
Assert.True(ran1);
309+
Assert.NotNull(serverOptions.DefaultCertificate);
310+
}
311+
312+
[Fact]
313+
public void ConfigureEndpoint_ThrowsWhen_TheKeyCannotBeRead()
314+
{
315+
var serverOptions = CreateServerOptions();
316+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
317+
318+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
319+
{
320+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
321+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
322+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key"))
323+
}).Build();
324+
325+
var ex = Assert.Throws<InvalidOperationException>(() =>
326+
{
327+
serverOptions
328+
.Configure(config)
329+
.Endpoint("End1", opt =>
330+
{
331+
Assert.True(opt.IsHttps);
332+
}).Load();
333+
});
334+
Assert.Equal(CoreStrings.InvalidPemKey, ex.Message);
335+
}
336+
337+
[Fact]
338+
public void ConfigureEndpoint_CanLoadDsaPemCerts()
339+
{
340+
var serverOptions = CreateServerOptions();
341+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-dsa.crt"));
342+
343+
var ran1 = false;
344+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
345+
{
346+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
347+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-dsa.crt")),
348+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-dsa.key")),
349+
new KeyValuePair<string, string>("Certificates:Default:Password", "asdf"),
350+
}).Build();
351+
352+
serverOptions
353+
.Configure(config)
354+
.Endpoint("End1", opt =>
355+
{
356+
ran1 = true;
357+
Assert.True(opt.IsHttps);
358+
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
359+
}).Load();
360+
361+
Assert.True(ran1);
362+
Assert.NotNull(serverOptions.DefaultCertificate);
363+
}
364+
365+
[Fact]
366+
public void ConfigureEndpoint_CanLoadUnprotectedDsaPemCerts()
367+
{
368+
var serverOptions = CreateServerOptions();
369+
var certificate = new X509Certificate2(TestResources.GetCertPath("https-dsa.crt"));
370+
371+
var ran1 = false;
372+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
373+
{
374+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
375+
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-dsa.crt")),
376+
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-dsa-no-pass.key")),
377+
}).Build();
378+
379+
serverOptions
380+
.Configure(config)
381+
.Endpoint("End1", opt =>
382+
{
383+
ran1 = true;
384+
Assert.True(opt.IsHttps);
385+
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
386+
}).Load();
387+
388+
Assert.True(ran1);
389+
Assert.NotNull(serverOptions.DefaultCertificate);
390+
}
391+
257392
[Fact]
258393
public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPasswordIsNotCorrect()
259394
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
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" />
1113
<Content Include="$(KestrelRoot)Core\src\Internal\Http\HttpHeaders.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
1214
<Content Include="$(KestrelRoot)Core\src\Internal\Http\HttpProtocol.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
1315
<Content Include="$(KestrelRoot)Core\src\Internal\Infrastructure\HttpUtilities.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />

0 commit comments

Comments
 (0)