-
Notifications
You must be signed in to change notification settings - Fork 109
Bind Kestrel options to config by default (#30) #44
Conversation
@CesarBS, |
src/Microsoft.AspNetCore/WebHost.cs
Outdated
@@ -148,6 +150,7 @@ public static IWebHostBuilder CreateDefaultBuilder(string[] args) | |||
|
|||
if (env.IsDevelopment()) | |||
{ | |||
Console.WriteLine(env.ApplicationName); |
There was a problem hiding this comment.
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)" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Undo this change?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @javiercn
There was a problem hiding this comment.
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>(); |
There was a problem hiding this comment.
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 => |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
).
samples/AppSettings/appsettings.json
Outdated
"Port": 8081 | ||
}, | ||
"HttpV6": { | ||
"Address": "[::1]", |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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"]; |
There was a problem hiding this comment.
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" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)"); |
There was a problem hiding this comment.
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)"); |
There was a problem hiding this comment.
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?
@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? |
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. |
Removed EKU check. Needs further discussion. |
.OrderByDescending(certificate => certificate.NotAfter) | ||
.FirstOrDefault(); | ||
|
||
#if NET46 |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
culture, style
@halter73 Added you as a reviewer. |
@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? |
@JunTaoLuo @Tratcher Can you guys review the functional tests I've just added? |
There was a problem hiding this 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.
There was a problem hiding this 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; |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#30