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

Support more certificate loading scenarios (#69) #85

Merged
merged 5 commits into from
May 2, 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
6 changes: 0 additions & 6 deletions MetaPackages.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ 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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-archive", "src\dotnet-archive\dotnet-archive.csproj", "{AE4216BF-D471-471B-82F3-6B6D004F7D17}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Archive", "src\Microsoft.DotNet.Archive\Microsoft.DotNet.Archive.csproj", "{302400A0-98BB-4C04-88D4-C32DC2D4B945}"
Expand Down Expand Up @@ -121,7 +116,6 @@ Global
{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}
{AE4216BF-D471-471B-82F3-6B6D004F7D17} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}
{302400A0-98BB-4C04-88D4-C32DC2D4B945} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}
{67E4C92F-6D12-4C52-BB79-B8D11BFC6B82} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}
Expand Down
1 change: 1 addition & 0 deletions build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<AspNetCoreIdentityServiceVersion>1.0.0-*</AspNetCoreIdentityServiceVersion>
<CoreFxVersion>4.3.0</CoreFxVersion>
<InternalAspNetCoreSdkVersion>2.0.0-*</InternalAspNetCoreSdkVersion>
<MoqVersion>4.7.1</MoqVersion>
<NewtonsoftJsonVersion>10.0.1</NewtonsoftJsonVersion>
<TestSdkVersion>15.0.0</TestSdkVersion>
<XunitVersion>2.2.0</XunitVersion>
Expand Down
2 changes: 1 addition & 1 deletion samples/AppSettings/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"Source": "File",
"Path": "testCert.pfx",
// TODO: remove when dotnet user-secrets is working again
"Password": "testPassword",
"Password": "testPassword"
}
},
// Add testCert.pfx to the current user's certificate store to enable this scenario.
Expand Down
15 changes: 15 additions & 0 deletions src/Microsoft.AspNetCore/CertificateFileLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.Security.Cryptography.X509Certificates;

namespace Microsoft.AspNetCore
{
internal class CertificateFileLoader : ICertificateFileLoader
{
public X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags)
{
return new X509Certificate2(path, password, flags);
}
}
}
190 changes: 115 additions & 75 deletions src/Microsoft.AspNetCore/CertificateLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,125 @@ 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
public class CertificateLoader
{
private readonly IConfiguration _certificatesConfiguration;
private readonly ICertificateFileLoader _certificateFileLoader;
private readonly ICertificateStoreLoader _certificateStoreLoader;

/// <summary>
/// Creates a new instance of <see cref="CertificateLoader"/>.
/// </summary>
public CertificateLoader()
: this(null)
{
}

/// <summary>
/// Creates a new instance of <see cref="CertificateLoader"/> that can load certificate references from configuration.
/// </summary>
/// <param name="certificatesConfiguration">An <see cref="IConfiguration"/> with information about certificates.</param>
public CertificateLoader(IConfiguration certificatesConfiguration)
: this(certificatesConfiguration, new CertificateFileLoader(), new CertificateStoreLoader())
{
_certificatesConfiguration = certificatesConfiguration;
}

internal CertificateLoader(IConfiguration certificatesConfiguration, ICertificateFileLoader certificateFileLoader, ICertificateStoreLoader certificateStoreLoader)
{
_certificatesConfiguration = certificatesConfiguration;
_certificateFileLoader = certificateFileLoader;
_certificateStoreLoader = certificateStoreLoader;
}

/// <summary>
/// Loads one or more certificates based on the information found in a configuration section.
/// </summary>
/// <param name="certificateConfiguration">A configuration section containing either a string value referencing certificates
/// by name, or one or more inline certificate specifications.
/// </param>
/// <returns>One or more loaded certificates.</returns>
public IEnumerable<X509Certificate2> Load(IConfigurationSection certificateConfiguration)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@danroth27 Do you want to keep the keys when loading from e.g.

"MyCert" : {
  "Source": "File",
  "Path": "mycert.pfx"
},
"MyCert2": {
  "Source": "Store",
  "StoreName": "My",
  "StoreLocation": "CurrentUser"
}

?

Copy link
Member

Choose a reason for hiding this comment

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

By "keys" do you mean the certificate name used in config? Yeah, I think it would be good to provide those.

Copy link
Member

Choose a reason for hiding this comment

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

But not required.

{
var certificateNames = certificateConfiguration.Value;
var certificates = new List<X509Certificate2>();

if (certificateNames != null)
{
foreach (var certificateName in certificateNames.Split(';'))
{
var certificate = LoadSingle(certificateName);
if (certificate != null)
{
certificates.Add(certificate);
}
}
}
else
{
if (certificateConfiguration["Source"] != null)
{
var certificate = LoadSingle(certificateConfiguration);
if (certificate != null)
{
certificates.Add(certificate);
}
}
else
{
certificates.AddRange(LoadMultiple(certificateConfiguration));
}
}

return certificates;
}

/// <summary>
/// Loads one or more certificates from a single source.
/// Loads a certificate by name.
/// </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)
/// <param name="certificateName">The certificate name.</param>
/// <returns>The loaded certificate</returns>
/// <remarks>This method only works if the <see cref="CertificateLoader"/> instance was constructed with
/// a reference to an <see cref="IConfiguration"/> instance containing named certificates.
/// </remarks>
private X509Certificate2 LoadSingle(string certificateName)
{
var certificateConfiguration = _certificatesConfiguration?.GetSection(certificateName);

if (!certificateConfiguration.Exists())
{
throw new InvalidOperationException($"No certificate named {certificateName} found in configuration");
}

return LoadSingle(certificateConfiguration);
}

private X509Certificate2 LoadSingle(IConfigurationSection certificateConfiguration)
{
var sourceKind = certificateConfiguration.GetValue<string>("Source");
var sourceKind = certificateConfiguration["Source"];

CertificateSource certificateSource;
switch (sourceKind.ToLowerInvariant())
{
case "file":
certificateSource = new CertificateFileSource(password);
certificateSource = new CertificateFileSource(_certificateFileLoader);
break;
case "store":
certificateSource = new CertificateStoreSource();
certificateSource = new CertificateStoreSource(_certificateStoreLoader);
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 IEnumerable<X509Certificate2> LoadMultiple(IConfigurationSection certificatesConfiguration)
=> certificatesConfiguration.GetChildren()
.Select(LoadSingle)
.Where(c => c != null);

private abstract class CertificateSource
{
Expand All @@ -65,22 +141,24 @@ private abstract class CertificateSource

private class CertificateFileSource : CertificateSource
{
private readonly string _password;
private ICertificateFileLoader _certificateFileLoader;

public CertificateFileSource(string password)
public CertificateFileSource(ICertificateFileLoader certificateFileLoader)
{
_password = password;
_certificateFileLoader = certificateFileLoader;
}

public string Path { get; set; }

public string Password { get; set; }

public override X509Certificate2 Load()
{
var certificate = TryLoad(X509KeyStorageFlags.DefaultKeySet, out var error)
?? TryLoad(X509KeyStorageFlags.UserKeySet, out error)
#if NETCOREAPP2_0
#if NETCOREAPP2_0
?? TryLoad(X509KeyStorageFlags.EphemeralKeySet, out error)
#endif
#endif
;

if (error != null)
Expand All @@ -95,7 +173,7 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except
{
try
{
var loadedCertificate = new X509Certificate2(Path, _password, flags);
var loadedCertificate = _certificateFileLoader.Load(Path, Password, flags);
exception = null;
return loadedCertificate;
}
Expand All @@ -109,6 +187,13 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except

private class CertificateStoreSource : CertificateSource
{
private readonly ICertificateStoreLoader _certificateStoreLoader;

public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader)
{
_certificateStoreLoader = certificateStoreLoader;
}

public string Subject { get; set; }
public string StoreName { get; set; }
public string StoreLocation { get; set; }
Expand All @@ -121,52 +206,7 @@ public override X509Certificate2 Load()
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();
}
}
return _certificateStoreLoader.Load(Subject, StoreName, storeLocation, !AllowInvalid);
}
}
}
Expand Down
56 changes: 56 additions & 0 deletions src/Microsoft.AspNetCore/CertificateStoreLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.Linq;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.AspNetCore
{
internal class CertificateStoreLoader : ICertificateStoreLoader
{
public X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly)
{
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);
foundCertificate = foundCertificates
.OfType<X509Certificate2>()
.OrderByDescending(certificate => certificate.NotAfter)
.FirstOrDefault();

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();
}
}
}
}
}
12 changes: 12 additions & 0 deletions src/Microsoft.AspNetCore/ICertificateFileLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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.Security.Cryptography.X509Certificates;

namespace Microsoft.AspNetCore
{
internal interface ICertificateFileLoader
{
X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags);
}
}
12 changes: 12 additions & 0 deletions src/Microsoft.AspNetCore/ICertificateStoreLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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.Security.Cryptography.X509Certificates;

namespace Microsoft.AspNetCore
{
internal interface ICertificateStoreLoader
{
X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly);
}
}
Loading