Skip to content

Commit

Permalink
Re-add support for .pem certificates with workaround for dotnet/coref…
Browse files Browse the repository at this point in the history
…x#24454
  • Loading branch information
natemcmaster committed May 11, 2018
1 parent 9e65b88 commit 94ca3a9
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 0.4.0.{build}
version: 0.4.1.{build}
install:
- ps: iwr https://raw.githubusercontent.com/dotnet/cli/release/2.1.3xx/scripts/obtain/dotnet-install.ps1 -outfile dotnet-install.ps1
- ps: .\dotnet-install.ps1 -Version 2.1.300-rc1-008673 -InstallDir $env:ProgramFiles/dotnet
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ Options:
`dotnet-serve` supports serving requests over HTTPS. You can configure the certificates used for HTTPS in the
following ways.

### .pem files

Use this when you have your certficate and private key stored in separate files (PEM encoded).
```
dotnet serve --cert ./cert.pem --key ./private.pem
```

Note: currently only RSA private keys are supported.

### .pfx file

Use this when you have your certficate as a .pfx/.p12 file (PKCS#12 format).
Expand All @@ -78,5 +87,6 @@ dotnet serve -S
If you just run `dotnet serve -S`, it will attempt to find a .pfx or ASP.NET Core dev cert automatically.

It will look for, in order:
- A pair of files named `cert.pem` and `private.key` in the current directory
- A file named `cert.pfx` in the current directory
- The ASP.NET Core Developer Certificate
10 changes: 9 additions & 1 deletion src/dotnet-serve/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,19 @@ public virtual bool UseTls
return _useTls.Value;
}

return !string.IsNullOrEmpty(CertPfxPath);
return !string.IsNullOrEmpty(CertPfxPath) || !string.IsNullOrEmpty(CertPemPath);;
}
private set => _useTls = value;
}

[Option("--cert", Description = "A PEM encoded certificate file to use for HTTPS connections.\nDefaults to file in current directory named '" + CertificateLoader.DefaultCertPemFileName + "'")]
[FileExists]
public string CertPemPath { get; }

[Option("--key", Description = "A PEM encoded private key to use for HTTPS connections.\nDefaults to file in current directory named '" + CertificateLoader.DefaultPrivateKeyFileName + "'")]
[FileExists]
public string PrivateKeyPath { get; }

[Option("--pfx", Description = "A PKCS#12 certificate file to use for HTTPS connections.\nDefaults to file in current directory named '" + CertificateLoader.DefaultCertPfxFileName + "'")]
[FileExists]
public string CertPfxPath { get; }
Expand Down
84 changes: 84 additions & 0 deletions src/dotnet-serve/Security/CertificateLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;

namespace McMaster.DotNet.Serve
{
Expand All @@ -14,6 +21,8 @@ class CertificateLoader
private const string AspNetHttpsOid = "1.3.6.1.4.1.311.84.1.1";
private const string AspNetHttpsOidFriendlyName = "ASP.NET Core HTTPS development certificate";

public const string DefaultCertPemFileName = "cert.pem";
public const string DefaultPrivateKeyFileName = "private.key";
public const string DefaultCertPfxFileName = "cert.pfx";

public static X509Certificate2 LoadCertificate(CommandLineOptions options, string currentDirectory)
Expand Down Expand Up @@ -41,6 +50,25 @@ private static X509Certificate2 FindCertificate(CommandLineOptions options, stri
return LoadFromPfxFile(options.CertPfxPath, options.CertificatePassword);
}

if (!string.IsNullOrEmpty(options.CertPemPath))
{
options.ExcludedFiles.Add(options.CertPemPath);
var privateKeyPath = !string.IsNullOrEmpty(options.PrivateKeyPath)
? options.PrivateKeyPath
: Path.Combine(Path.GetDirectoryName(options.CertPemPath), DefaultPrivateKeyFileName);
options.ExcludedFiles.Add(privateKeyPath);
return LoadFromPfxFile(options.CertPemPath, privateKeyPath);
}

var defaultCertFile = Path.Combine(currentDirectory, DefaultCertPemFileName);
var defaultKeyFile = Path.Combine(currentDirectory, DefaultPrivateKeyFileName);
if (File.Exists(defaultCertFile) && File.Exists(defaultKeyFile))
{
options.ExcludedFiles.Add(defaultCertFile);
options.ExcludedFiles.Add(defaultKeyFile);
return LoadFromPem(defaultCertFile, defaultKeyFile);
}

var defaultPfxFile = Path.Combine(currentDirectory, DefaultCertPfxFileName);
if (File.Exists(defaultPfxFile))
{
Expand Down Expand Up @@ -68,6 +96,62 @@ private static X509Certificate2 LoadFromPfxFile(string filepath, string password
}
}

private static X509Certificate2 LoadFromPem(string certPath, string keyPath)
{
try
{
using (var certWithoutPrivateKey = new X509Certificate2(certPath))
using (var keyFile = File.OpenText(keyPath))
{
// Workaround https://github.com/dotnet/corefx/issues/20414
var pemReader = new PemReader(keyFile);

var pemObj = pemReader.ReadObject();
switch (pemObj)
{
case RsaPrivateCrtKeyParameters rsaParams:
{
var rsa = CreateRSA(rsaParams);
// See https://github.com/dotnet/corefx/issues/24454#issuecomment-388231655
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
using (var certWithKey = certWithoutPrivateKey.CopyWithPrivateKey(rsa))
{
return new X509Certificate2(certWithKey.Export(X509ContentType.Pkcs12));
}
}
else
{
// Only works on Linux/macOS
return certWithoutPrivateKey.CopyWithPrivateKey(rsa);
}
}
}

throw new InvalidOperationException($"Failed to read private key from '{keyPath}'. Unexpected format: " + pemObj.GetType().Name);
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to load certificate file from '{certPath}' and '{keyPath}'", ex);
}
}

private static RSA CreateRSA(RsaPrivateCrtKeyParameters rsaParams)
{
return RSA.Create(new RSAParameters
{
Modulus = rsaParams.Modulus.ToByteArray(),
Exponent = rsaParams.PublicExponent.ToByteArray(),
D = rsaParams.Exponent.ToByteArray(),
P = rsaParams.P.ToByteArray(),
Q = rsaParams.Q.ToByteArray(),
DP = rsaParams.DP.ToByteArray(),
DQ = rsaParams.DQ.ToByteArray(),
InverseQ = rsaParams.QInv.ToByteArray(),
});
}

private static X509Certificate2 LoadDeveloperCertificate()
{
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
Expand Down
1 change: 1 addition & 0 deletions src/dotnet-serve/dotnet-serve.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.2.2" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.2" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions test/dotnet-serve.Tests/CertLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,36 @@ public async Task ItDoesNotServePfxFile()
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
}


[Theory]
[InlineData("rsa", "E8481D606B15080024C806EFE89B00F0976BD906")]
public void ItLoadsPemAndKeyFileByDefault(string keyFormat, string thumbprint)
{
var path = Path.Combine(AppContext.BaseDirectory, "TestAssets", "Https", keyFormat);
var options = new Mock<CommandLineOptions>();
options.SetupGet(o => o.UseTls).Returns(true);
var x509 = CertificateLoader.LoadCertificate(options.Object, path);
Assert.NotNull(x509);
Assert.Equal(thumbprint, x509.Thumbprint);
Assert.True(x509.HasPrivateKey, "Cert should have private key");
}

[Theory]
[InlineData("rsa")]
public async Task ItDoesNotServePemFiles(string keyFormat)
{
var path = Path.Combine(AppContext.BaseDirectory, "TestAssets", "Https", keyFormat);
using (var ds = DotNetServe.Start(path,
output: _output,
enableTls: true))
{
var resp1 = await ds.Client.GetWithRetriesAsync("/cert.pem");
Assert.Equal(HttpStatusCode.NotFound, resp1.StatusCode);

var resp2 = await ds.Client.GetWithRetriesAsync("/private.key");
Assert.Equal(HttpStatusCode.NotFound, resp2.StatusCode);
}
}
}
}
1 change: 1 addition & 0 deletions test/dotnet-serve.Tests/dotnet-serve.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.2" />
<PackageReference Include="Moq" Version="4.8.2" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion version.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>0.4.0</VersionPrefix>
<VersionPrefix>0.4.1</VersionPrefix>
<VersionSuffix>build</VersionSuffix>
<SourceRevisionId>$(APPVEYOR_REPO_COMMIT)</SourceRevisionId>
<PackageVersion Condition="'$(APPVEYOR_REPO_TAG)' == 'true'">$(VersionPrefix)</PackageVersion>
Expand Down

0 comments on commit 94ca3a9

Please sign in to comment.