Skip to content
This repository was archived by the owner on Nov 21, 2018. It is now read-only.

Bind Kestrel options to config by default (#30) #44

Merged
merged 18 commits into from
Apr 26, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 14 additions & 1 deletion MetaPackages.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26419.0
VisualStudioVersion = 15.0.26424.2
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}"
EndProject
Expand All @@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{192F
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{AF5BB04E-92F7-4737-8B98-F86F6244FAB2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppSettings", "samples\AppSettings\AppSettings.csproj", "{5009D7C8-6061-49CF-9A30-23B309BBEFB0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.FunctionalTests", "test\Microsoft.AspNetCore.Tests\Microsoft.AspNetCore.FunctionalTests.csproj", "{C72A756A-D29D-44C7-83D4-821DBE82DBCA}"
Expand All @@ -38,6 +40,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StartRequestDelegateUrlApp"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateDefaultBuilderApp", "test\TestSites\CreateDefaultBuilderApp\CreateDefaultBuilderApp.csproj", "{79CF58CE-B020-45D8-BDB5-2D8036BEAD14}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestArtifacts", "TestArtifacts", "{9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90}"
ProjectSection(SolutionItems) = preProject
test\TestArtifacts\testCert.pfx = test\TestArtifacts\testCert.pfx
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -56,6 +63,10 @@ Global
{AF5BB04E-92F7-4737-8B98-F86F6244FAB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF5BB04E-92F7-4737-8B98-F86F6244FAB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF5BB04E-92F7-4737-8B98-F86F6244FAB2}.Release|Any CPU.Build.0 = Release|Any CPU
{5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5009D7C8-6061-49CF-9A30-23B309BBEFB0}.Release|Any CPU.Build.0 = Release|Any CPU
{C72A756A-D29D-44C7-83D4-821DBE82DBCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C72A756A-D29D-44C7-83D4-821DBE82DBCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C72A756A-D29D-44C7-83D4-821DBE82DBCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -85,11 +96,13 @@ Global
{CC8F551E-213A-45E8-AECA-507C4DB4F164} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}
{F92CB7A1-C38E-408C-A7EC-A5C040D041E1} = {97D53BEB-A511-4FBE-B784-AB407D9A219F}
{AF5BB04E-92F7-4737-8B98-F86F6244FAB2} = {192F583C-C4CA-43E5-B31C-D21B7806E274}
{5009D7C8-6061-49CF-9A30-23B309BBEFB0} = {192F583C-C4CA-43E5-B31C-D21B7806E274}
{C72A756A-D29D-44C7-83D4-821DBE82DBCA} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C}
{EC22261D-0DE1-47DE-8F7C-072675D6F5B4} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C}
{AB42054B-1801-4FEE-B5C3-8529C6D7BFDA} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
{3A85FA52-F601-422E-A42E-9F187DB28492} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
{401C741B-6C7C-4E08-9F09-C3D43D22C0DE} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
{79CF58CE-B020-45D8-BDB5-2D8036BEAD14} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
{9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C}
EndGlobalSection
EndGlobal
23 changes: 23 additions & 0 deletions samples/AppSettings/AppSettings.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<Import Project="..\..\build\dependencies.props" />

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<UserSecretsId>aspnetcore-MetaPackagesAppSettings-20170421155031</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<Content Include="testCert.pfx" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore\Microsoft.AspNetCore.csproj" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="$(AspNetCoreVersion)" />
</ItemGroup>

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="$(AspNetCoreVersion)" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions samples/AppSettings/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Http;

namespace AppSettings
{
public class Program
{
public static void Main(string[] args)
Copy link
Member

Choose a reason for hiding this comment

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

Why not use the existing sample?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Settings would apply to all the scenarios there. I want to keep things separate for clarity.

{
using (WebHost.Start(context => context.Response.WriteAsync("Hello, World!")))
{
Console.WriteLine("Running application: Press any key to shutdown...");
Console.ReadKey();
}
}
}
}
27 changes: 27 additions & 0 deletions samples/AppSettings/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53434/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"AppSettings": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:53435"
}
}
}
63 changes: 63 additions & 0 deletions samples/AppSettings/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"Kestrel": {
"EndPoints": {
"Http": {
"Address": "127.0.0.1",
"Port": 8081
},
"HttpV6": {
"Address": "::1",
"Port": 8081
},
// Add testCert.pfx to the current user's certificate store to enable this scenario.
//"HttpsInlineCertStore": {
// "Address": "127.0.0.1",
// "Port": 8082,
// "Certificate": {
// "Source": "Store",
// "Subject": "cn=localhost",
// "StoreName": "My",
// "StoreLocation": "CurrentUser",
// "AllowInvalid": "True"
// }
//},
"HttpsInlineCertFile": {
"Address": "127.0.0.1",
"Port": 8083,
"Certificate": {
"Source": "File",
"Path": "testCert.pfx",
// TODO: remove when dotnet user-secrets is working again
"Password": "testPassword",
}
},
// Add testCert.pfx to the current user's certificate store to enable this scenario.
//"HttpsCertStore": {
// "Address": "127.0.0.1",
// "Port": 8084,
// "Certificate": "TestCertInStore"
//},
"HttpsCertFile": {
"Address": "127.0.0.1",
"Port": 8085,
"Certificate": "TestCert"
}
}
},
"Certificates": {
"TestCert": {
"Source": "File",
"Path": "testCert.pfx",
// TODO: remove when dotnet user-secrets is working again
"Password": "testPassword"
},
// Add testCert.pfx to the current user's certificate store to enable this scenario.
//"TestCertInStore": {
// "Source": "Store",
// "Subject": "cn=localhost",
// "StoreName": "My",
// "StoreLocation": "CurrentUser",
// "AllowInvalid": "True"
//}
}
}
Binary file added samples/AppSettings/testCert.pfx
Binary file not shown.
2 changes: 1 addition & 1 deletion samples/SampleApp/SampleApp.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<Import Project="..\..\build\common.props" />
<Import Project="..\..\build\dependencies.props" />
Copy link
Member

Choose a reason for hiding this comment

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

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@natemcmaster pointed out we don't normally import common.props here, only dependencies.props. This was causing issues with user secrets because common.props has GenerateUserSecretsAttribute set to false.


<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
Expand Down
173 changes: 173 additions & 0 deletions src/Microsoft.AspNetCore/CertificateLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;

namespace Microsoft.AspNetCore
{
/// <summary>
/// A helper class to load certificates from files and certificate stores based on <seealso cref="IConfiguration"/> data.
/// </summary>
public static class CertificateLoader
{
/// <summary>
/// Loads one or more certificates from a single source.
/// </summary>
/// <param name="certificateConfiguration">An <seealso cref="IConfiguration"/> with information about a certificate source.</param>
/// <param name="password">The certificate password, in case it's being loaded from a file.</param>
/// <returns>The loaded certificates.</returns>
public static X509Certificate2 Load(IConfiguration certificateConfiguration, string password)
{
var sourceKind = certificateConfiguration.GetValue<string>("Source");

CertificateSource certificateSource;
switch (sourceKind.ToLowerInvariant())
{
case "file":
certificateSource = new CertificateFileSource(password);
break;
case "store":
certificateSource = new CertificateStoreSource();
break;
default:
throw new InvalidOperationException($"Invalid certificate source kind: {sourceKind}");
}

certificateConfiguration.Bind(certificateSource);
return certificateSource.Load();
}

/// <summary>
/// Loads all certificates specified in an <seealso cref="IConfiguration"/>.
/// </summary>
/// <param name="configurationRoot">The root <seealso cref="IConfiguration"/>.</param>
/// <returns>
/// A dictionary mapping certificate names to loaded certificates.
/// </returns>
public static Dictionary<string, X509Certificate2> LoadAll(IConfiguration configurationRoot)
{
return configurationRoot.GetSection("Certificates").GetChildren()
.ToDictionary(
certificateSource => certificateSource.Key,
certificateSource => Load(certificateSource, certificateSource["Password"]));
}

private abstract class CertificateSource
{
public string Source { get; set; }

public abstract X509Certificate2 Load();
}

private class CertificateFileSource : CertificateSource
{
private readonly string _password;

public CertificateFileSource(string password)
{
_password = password;
}

public string Path { get; set; }

public override X509Certificate2 Load()
{
var certificate = TryLoad(X509KeyStorageFlags.DefaultKeySet, out var error)
?? TryLoad(X509KeyStorageFlags.UserKeySet, out error)
#if NETCOREAPP2_0
?? TryLoad(X509KeyStorageFlags.EphemeralKeySet, out error)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@blowdart Is this the proper order to attempt to load the cert? cc @javiercn

Copy link
Member

Choose a reason for hiding this comment

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

What are you trying to do here? @bartonjs because he understands Ephemeral :)

Choose a reason for hiding this comment

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

I read this as:

  • Load perphemeral, as MachineKeySet as the PFX says so, or as UserKeySet if it has no opinion on the matter.
  • Load perphemeral as UserKeySet (in case the key was MachineKeySet and the current process does not have write access to the machine key store).
  • Load ephemeral (in case the current process has no persisted key write access at all, such as low rights with no user profile)

Copy link
Member

Choose a reason for hiding this comment

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

"perphemeral"? 🤣

Is there an advantage in trying "perphemeral" first, if ephermeral is there? There's no real need to persist the private key anyhere.

Choose a reason for hiding this comment

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

Yup, I wordsmithed.

Assuming ephemeral works for your needs, it's probably the best choice. It takes disk IO and permissions completely out of play.

The only concern is that if you pass the cert to something which directly calls CertGetCertificateContextProperty with CERT_KEY_PROV_INFO_PROP_ID they will be confused. But it avoids the whole we-leak-a-file-to-the-hard-drive-if-the-process-crashes annoyance of perphemeral (and the disk I/O // wear-and-tear).

EphemeralKeySet doesn't work on macOS, though, so you'll still want to fall back to a perphemeral mode there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll put the call with EphemeralKeySet first then, with a fallback to UserKeySet if that fails. Does that sound right?

#endif
;

if (error != null)
{
throw error;
Copy link
Member

Choose a reason for hiding this comment

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

Rethrowing this exception looses the original stack trace. You should wrap it or use ExceptionDispatchInfo to capture and rethrow it.

}

return certificate;
}

private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception exception)
{
try
{
var loadedCertificate = new X509Certificate2(Path, _password, flags);
exception = null;
return loadedCertificate;
}
catch (Exception e)
{
exception = e;
return null;
}
}
}

private class CertificateStoreSource : CertificateSource
{
public string Subject { get; set; }
public string StoreName { get; set; }
public string StoreLocation { get; set; }
public bool AllowInvalid { get; set; }

public override X509Certificate2 Load()
{
if (!Enum.TryParse(StoreLocation, ignoreCase: true, result: out StoreLocation storeLocation))
{
throw new InvalidOperationException($"Invalid store location: {StoreLocation}");
}

using (var store = new X509Store(StoreName, storeLocation))
{
X509Certificate2Collection storeCertificates = null;
X509Certificate2Collection foundCertificates = null;
X509Certificate2 foundCertificate = null;

try
{
store.Open(OpenFlags.ReadOnly);
storeCertificates = store.Certificates;
foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectDistinguishedName, Subject, validOnly: !AllowInvalid);
foundCertificate = foundCertificates
.OfType<X509Certificate2>()
.OrderByDescending(certificate => certificate.NotAfter)
.FirstOrDefault();

if (foundCertificate == null)
{
throw new InvalidOperationException($"No certificate found for {Subject} in store {StoreName} in {StoreLocation}");
}

return foundCertificate;
}
finally
{
if (foundCertificate != null)
{
storeCertificates.Remove(foundCertificate);
foundCertificates.Remove(foundCertificate);
}

DisposeCertificates(storeCertificates);
DisposeCertificates(foundCertificates);
}
}
}

private void DisposeCertificates(X509Certificate2Collection certificates)
{
if (certificates != null)
{
foreach (var certificate in certificates)
{
certificate.Dispose();
}
}
}
}
}
}
Loading