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

Conversation

cesarblum
Copy link
Contributor

@cesarblum cesarblum commented Apr 24, 2017

#30

@dnfclas
Copy link

dnfclas commented Apr 24, 2017

@CesarBS,
Thanks for having already signed the Contribution License Agreement. Your agreement was validated by .NET Foundation. We will now review your pull request.
Thanks,
.NET Foundation Pull Request Bot

@@ -148,6 +150,7 @@ public static IWebHostBuilder CreateDefaultBuilder(string[] args)

if (env.IsDevelopment())
{
Console.WriteLine(env.ApplicationName);
Copy link
Member

Choose a reason for hiding this comment

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

I assume this is just left over.

@@ -11,7 +11,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="@(MetaPackagePackageReference)" />
<PackageReference Include="@(MetaPackagePackageReference)" />
Copy link
Member

Choose a reason for hiding this comment

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

Undo this change?

Copy link
Contributor Author

@cesarblum cesarblum Apr 24, 2017

Choose a reason for hiding this comment

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

Indentation was wrong.

/// </summary>
public class KestrelServerOptionsSetup : IConfigureOptions<KestrelServerOptions>
{
private IServiceProvider _services;
Copy link
Member

Choose a reason for hiding this comment

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

This should be IConfiguration, not IServiceProvider

/// Creates a new instance of <see cref="KestrelServerOptionsSetup"/>.
/// </summary>
/// <param name="services">An <seealso cref="IServiceProvider"/> instance.</param>
public KestrelServerOptionsSetup(IServiceProvider services)
Copy link
Member

@davidfowl davidfowl Apr 24, 2017

Choose a reason for hiding this comment

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

See comment above. Same thing. Inject the IConfiguration directly.

var sourceKind = certificateConfiguration.GetValue<string>("Source");

CertificateSource certificateSource;
switch (sourceKind)
Copy link
Member

Choose a reason for hiding this comment

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

Is this supposed to be case sensitive?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

No, avoid treating config as case sensitive.

public static Dictionary<string, X509Certificate2> LoadAll(IConfiguration configurationRoot)
{
var certificates = configurationRoot.GetSection("Certificates");
var loadedCertificates = new Dictionary<string, X509Certificate2>();
Copy link
Member

Choose a reason for hiding this comment

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

This looks like it could be a single LINQ statement.

IConfigurationSection endPoint,
Dictionary<string, X509Certificate2> certificates)
{
options.Listen(IPAddress.Parse(endPoint.GetValue<string>("Address")), endPoint.GetValue<int>("Port"), listenOptions =>
Copy link
Member

Choose a reason for hiding this comment

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

This needs to be more failure tolerant. TryParse with a decent exception message

{
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.

var sourceKind = certificateConfiguration.GetValue<string>("Source");

CertificateSource certificateSource;
switch (sourceKind)
Copy link
Member

Choose a reason for hiding this comment

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

No, avoid treating config as case sensitive.


public override X509Certificate2 Load()
{
if (!Enum.TryParse(StoreLocation, true, out StoreLocation storeLocation))
Copy link
Member

Choose a reason for hiding this comment

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

named params

using (var store = new X509Store(StoreName, storeLocation))
{
store.Open(OpenFlags.ReadOnly);
var foundCertificate = store.Certificates.Find(X509FindType.FindBySubjectName, Subject, validOnly: false)
Copy link
Member

Choose a reason for hiding this comment

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

Is there an EKU check anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

Copy link
Member

Choose a reason for hiding this comment

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

Why validOnly: false? Do we want people to be able to bind to expired certs?

Choose a reason for hiding this comment

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

  • FindBySubjectName does strstr. If you want exact match you'll need to post-process it.
  • store.Certificates returns unique X509Certificate2 objects, and Find returns unique X509Certificate2 objects. If you would like to reduce Finalizations you should dispose all of the members of store.Certificates (which you need to save as a local, since reading it twice just makes the problem worse).

Similary, if you cascade call Find you should dispose all the previous values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@javiercn Since this was originally your code, what are your thoughts here?

Copy link
Member

Choose a reason for hiding this comment

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

@blowdart self-signed certificates. I would change that to HostingEnvironment.IsDevelopment() if its available

Copy link
Member

Choose a reason for hiding this comment

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

Another option is to just load the certificates and filter using Linq, or have an extra configuration value per certificate that indicates if its ok to load invalid (so that you can again load self-signed certificates for development)

Yet another option is to make the check true and force the user to put the certificate (in case its self-signed) in the trusted certificate store for .NET. (Which varies across platforms)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Talked to @danroth27 and we decided to go with a config key (AllowInvalid).

"Port": 8081
},
"HttpV6": {
"Address": "[::1]",
Copy link
Member

Choose a reason for hiding this comment

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

Brackets not required

X509Certificate2 endPointCertificate = null;
if (certificateName != null)
{
endPointCertificate = certificates[certificateName];
Copy link
Member

Choose a reason for hiding this comment

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

KeyNotFoundException? You can give a better error.


if (certificate.GetChildren().Any())
{
var password = _configurationRoot[$"Kestrel:EndPoints:{endPoint.Key}:Certificate:Password"];
Copy link
Member

Choose a reason for hiding this comment

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

is this not already part of the certificate config section? even if it's coming from a different config source it should get merged.

@@ -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.

{
try
{
var loadedCertificate = new X509Certificate2(Path, _password);
Copy link
Member

Choose a reason for hiding this comment

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

var loadedCertificate = new X509Certificate2(Path, _password, flags);

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?

if (!HasServerAuthEKU(certificate))
{
throw new InvalidOperationException(
$"The certificate file at '{Path}' does not contain an Enhanced Key Usage field with the OID 1.3.6.1.5.5.7.3.1 (Server Authentication)");
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 the wording correct?

if (foundCertificate == null)
{
throw new InvalidOperationException(
$"No valid certificate found for {Subject} in store {StoreName} under {StoreLocation}. Check that the certificate contains the Enhanced Key Usage field with the OID 1.3.6.1.5.5.7.3.1 (Server Authentication)");
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 the wording correct?

@cesarblum
Copy link
Contributor Author

cesarblum commented Apr 24, 2017

@danroth27 Should the EKU check be mandatory as I've done it here? Or should we add a flag to optionally disable it?

Should we actually move the EKU check to Kestrel? @blowdart @davidfowl Thoughts?

@danroth27
Copy link
Member

Yeah, if we're going to check the EKUs at all it should be done lower in the stack. Let's remove this check from here.

@cesarblum
Copy link
Contributor Author

Removed EKU check. Needs further discussion.

.OrderByDescending(certificate => certificate.NotAfter)
.FirstOrDefault();

#if NET46
Copy link
Member

Choose a reason for hiding this comment

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

No needed.

}

int port;
if (!int.TryParse(portValue, out port))
Copy link
Member

Choose a reason for hiding this comment

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

culture, style

@cesarblum cesarblum requested a review from halter73 April 25, 2017 18:14
@cesarblum
Copy link
Contributor Author

@halter73 Added you as a reviewer.

@blowdart
Copy link
Member

@danroth27 It's not a question of IF we perform an EKU check. We should/must. So if it's removed from here, then where is the corresponding issue in kestrel to perform the check there?

@cesarblum cesarblum changed the base branch from rel/1.1.0-preview1 to rel/2.0.0-preview1 April 26, 2017 21:34
@cesarblum
Copy link
Contributor Author

@JunTaoLuo @Tratcher Can you guys review the functional tests I've just added?

Copy link
Contributor

@JunTaoLuo JunTaoLuo left a comment

Choose a reason for hiding this comment

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

Functional test looks good. I only skimmed the rest of the code change though.

@cesarblum cesarblum merged commit 21de306 into rel/2.0.0-preview1 Apr 26, 2017
@cesarblum cesarblum deleted the cesarbs/30 branch April 26, 2017 23:47
Copy link
Member

@Tratcher Tratcher left a comment

Choose a reason for hiding this comment

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

This still needs work.


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.

var portValue = endPoint.GetValue<string>("Port");

IPAddress address;
if (!IPAddress.TryParse(addressValue, out address))
Copy link
Member

Choose a reason for hiding this comment

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

Wait, I can't bind to * or localhost? That's a non-starter.

Copy link
Member

Choose a reason for hiding this comment

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

#73

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.