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

Commit 605aedd

Browse files
author
Cesar Blum Silveira
authored
Support more certificate loading scenarios (#69).
1 parent a5a9b6a commit 605aedd

16 files changed

+1240
-134
lines changed

MetaPackages.sln

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StartRequestDelegateUrlApp"
4040
EndProject
4141
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateDefaultBuilderApp", "test\TestSites\CreateDefaultBuilderApp\CreateDefaultBuilderApp.csproj", "{79CF58CE-B020-45D8-BDB5-2D8036BEAD14}"
4242
EndProject
43-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestArtifacts", "TestArtifacts", "{9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90}"
44-
ProjectSection(SolutionItems) = preProject
45-
test\TestArtifacts\testCert.pfx = test\TestArtifacts\testCert.pfx
46-
EndProjectSection
47-
EndProject
4843
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-archive", "src\dotnet-archive\dotnet-archive.csproj", "{AE4216BF-D471-471B-82F3-6B6D004F7D17}"
4944
EndProject
5045
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Archive", "src\Microsoft.DotNet.Archive\Microsoft.DotNet.Archive.csproj", "{302400A0-98BB-4C04-88D4-C32DC2D4B945}"
@@ -121,7 +116,6 @@ Global
121116
{3A85FA52-F601-422E-A42E-9F187DB28492} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
122117
{401C741B-6C7C-4E08-9F09-C3D43D22C0DE} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
123118
{79CF58CE-B020-45D8-BDB5-2D8036BEAD14} = {EC22261D-0DE1-47DE-8F7C-072675D6F5B4}
124-
{9BBA7A0A-109A-4AC8-B6EF-A52EA7CF1D90} = {9E49B5B9-9E72-42FB-B684-90CA1B1BCF9C}
125119
{AE4216BF-D471-471B-82F3-6B6D004F7D17} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}
126120
{302400A0-98BB-4C04-88D4-C32DC2D4B945} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}
127121
{67E4C92F-6D12-4C52-BB79-B8D11BFC6B82} = {ED834E68-51C3-4ADE-ACC8-6BA6D4207C09}

build/dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<AspNetCoreIdentityServiceVersion>1.0.0-*</AspNetCoreIdentityServiceVersion>
66
<CoreFxVersion>4.3.0</CoreFxVersion>
77
<InternalAspNetCoreSdkVersion>2.0.0-*</InternalAspNetCoreSdkVersion>
8+
<MoqVersion>4.7.1</MoqVersion>
89
<NewtonsoftJsonVersion>10.0.1</NewtonsoftJsonVersion>
910
<TestSdkVersion>15.0.0</TestSdkVersion>
1011
<XunitVersion>2.2.0</XunitVersion>

samples/AppSettings/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"Source": "File",
2929
"Path": "testCert.pfx",
3030
// TODO: remove when dotnet user-secrets is working again
31-
"Password": "testPassword",
31+
"Password": "testPassword"
3232
}
3333
},
3434
// Add testCert.pfx to the current user's certificate store to enable this scenario.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace Microsoft.AspNetCore
7+
{
8+
internal class CertificateFileLoader : ICertificateFileLoader
9+
{
10+
public X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags)
11+
{
12+
return new X509Certificate2(path, password, flags);
13+
}
14+
}
15+
}

src/Microsoft.AspNetCore/CertificateLoader.cs

Lines changed: 115 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,125 @@ namespace Microsoft.AspNetCore
1212
/// <summary>
1313
/// A helper class to load certificates from files and certificate stores based on <seealso cref="IConfiguration"/> data.
1414
/// </summary>
15-
public static class CertificateLoader
15+
public class CertificateLoader
1616
{
17+
private readonly IConfiguration _certificatesConfiguration;
18+
private readonly ICertificateFileLoader _certificateFileLoader;
19+
private readonly ICertificateStoreLoader _certificateStoreLoader;
20+
21+
/// <summary>
22+
/// Creates a new instance of <see cref="CertificateLoader"/>.
23+
/// </summary>
24+
public CertificateLoader()
25+
: this(null)
26+
{
27+
}
28+
29+
/// <summary>
30+
/// Creates a new instance of <see cref="CertificateLoader"/> that can load certificate references from configuration.
31+
/// </summary>
32+
/// <param name="certificatesConfiguration">An <see cref="IConfiguration"/> with information about certificates.</param>
33+
public CertificateLoader(IConfiguration certificatesConfiguration)
34+
: this(certificatesConfiguration, new CertificateFileLoader(), new CertificateStoreLoader())
35+
{
36+
_certificatesConfiguration = certificatesConfiguration;
37+
}
38+
39+
internal CertificateLoader(IConfiguration certificatesConfiguration, ICertificateFileLoader certificateFileLoader, ICertificateStoreLoader certificateStoreLoader)
40+
{
41+
_certificatesConfiguration = certificatesConfiguration;
42+
_certificateFileLoader = certificateFileLoader;
43+
_certificateStoreLoader = certificateStoreLoader;
44+
}
45+
46+
/// <summary>
47+
/// Loads one or more certificates based on the information found in a configuration section.
48+
/// </summary>
49+
/// <param name="certificateConfiguration">A configuration section containing either a string value referencing certificates
50+
/// by name, or one or more inline certificate specifications.
51+
/// </param>
52+
/// <returns>One or more loaded certificates.</returns>
53+
public IEnumerable<X509Certificate2> Load(IConfigurationSection certificateConfiguration)
54+
{
55+
var certificateNames = certificateConfiguration.Value;
56+
var certificates = new List<X509Certificate2>();
57+
58+
if (certificateNames != null)
59+
{
60+
foreach (var certificateName in certificateNames.Split(';'))
61+
{
62+
var certificate = LoadSingle(certificateName);
63+
if (certificate != null)
64+
{
65+
certificates.Add(certificate);
66+
}
67+
}
68+
}
69+
else
70+
{
71+
if (certificateConfiguration["Source"] != null)
72+
{
73+
var certificate = LoadSingle(certificateConfiguration);
74+
if (certificate != null)
75+
{
76+
certificates.Add(certificate);
77+
}
78+
}
79+
else
80+
{
81+
certificates.AddRange(LoadMultiple(certificateConfiguration));
82+
}
83+
}
84+
85+
return certificates;
86+
}
87+
1788
/// <summary>
18-
/// Loads one or more certificates from a single source.
89+
/// Loads a certificate by name.
1990
/// </summary>
20-
/// <param name="certificateConfiguration">An <seealso cref="IConfiguration"/> with information about a certificate source.</param>
21-
/// <param name="password">The certificate password, in case it's being loaded from a file.</param>
22-
/// <returns>The loaded certificates.</returns>
23-
public static X509Certificate2 Load(IConfiguration certificateConfiguration, string password)
91+
/// <param name="certificateName">The certificate name.</param>
92+
/// <returns>The loaded certificate</returns>
93+
/// <remarks>This method only works if the <see cref="CertificateLoader"/> instance was constructed with
94+
/// a reference to an <see cref="IConfiguration"/> instance containing named certificates.
95+
/// </remarks>
96+
private X509Certificate2 LoadSingle(string certificateName)
97+
{
98+
var certificateConfiguration = _certificatesConfiguration?.GetSection(certificateName);
99+
100+
if (!certificateConfiguration.Exists())
101+
{
102+
throw new InvalidOperationException($"No certificate named {certificateName} found in configuration");
103+
}
104+
105+
return LoadSingle(certificateConfiguration);
106+
}
107+
108+
private X509Certificate2 LoadSingle(IConfigurationSection certificateConfiguration)
24109
{
25-
var sourceKind = certificateConfiguration.GetValue<string>("Source");
110+
var sourceKind = certificateConfiguration["Source"];
26111

27112
CertificateSource certificateSource;
28113
switch (sourceKind.ToLowerInvariant())
29114
{
30115
case "file":
31-
certificateSource = new CertificateFileSource(password);
116+
certificateSource = new CertificateFileSource(_certificateFileLoader);
32117
break;
33118
case "store":
34-
certificateSource = new CertificateStoreSource();
119+
certificateSource = new CertificateStoreSource(_certificateStoreLoader);
35120
break;
36121
default:
37122
throw new InvalidOperationException($"Invalid certificate source kind: {sourceKind}");
38123
}
39124

40125
certificateConfiguration.Bind(certificateSource);
126+
41127
return certificateSource.Load();
42128
}
43129

44-
/// <summary>
45-
/// Loads all certificates specified in an <seealso cref="IConfiguration"/>.
46-
/// </summary>
47-
/// <param name="configurationRoot">The root <seealso cref="IConfiguration"/>.</param>
48-
/// <returns>
49-
/// A dictionary mapping certificate names to loaded certificates.
50-
/// </returns>
51-
public static Dictionary<string, X509Certificate2> LoadAll(IConfiguration configurationRoot)
52-
{
53-
return configurationRoot.GetSection("Certificates").GetChildren()
54-
.ToDictionary(
55-
certificateSource => certificateSource.Key,
56-
certificateSource => Load(certificateSource, certificateSource["Password"]));
57-
}
130+
private IEnumerable<X509Certificate2> LoadMultiple(IConfigurationSection certificatesConfiguration)
131+
=> certificatesConfiguration.GetChildren()
132+
.Select(LoadSingle)
133+
.Where(c => c != null);
58134

59135
private abstract class CertificateSource
60136
{
@@ -65,22 +141,24 @@ private abstract class CertificateSource
65141

66142
private class CertificateFileSource : CertificateSource
67143
{
68-
private readonly string _password;
144+
private ICertificateFileLoader _certificateFileLoader;
69145

70-
public CertificateFileSource(string password)
146+
public CertificateFileSource(ICertificateFileLoader certificateFileLoader)
71147
{
72-
_password = password;
148+
_certificateFileLoader = certificateFileLoader;
73149
}
74150

75151
public string Path { get; set; }
76152

153+
public string Password { get; set; }
154+
77155
public override X509Certificate2 Load()
78156
{
79157
var certificate = TryLoad(X509KeyStorageFlags.DefaultKeySet, out var error)
80158
?? TryLoad(X509KeyStorageFlags.UserKeySet, out error)
81-
#if NETCOREAPP2_0
159+
#if NETCOREAPP2_0
82160
?? TryLoad(X509KeyStorageFlags.EphemeralKeySet, out error)
83-
#endif
161+
#endif
84162
;
85163

86164
if (error != null)
@@ -95,7 +173,7 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except
95173
{
96174
try
97175
{
98-
var loadedCertificate = new X509Certificate2(Path, _password, flags);
176+
var loadedCertificate = _certificateFileLoader.Load(Path, Password, flags);
99177
exception = null;
100178
return loadedCertificate;
101179
}
@@ -109,6 +187,13 @@ private X509Certificate2 TryLoad(X509KeyStorageFlags flags, out Exception except
109187

110188
private class CertificateStoreSource : CertificateSource
111189
{
190+
private readonly ICertificateStoreLoader _certificateStoreLoader;
191+
192+
public CertificateStoreSource(ICertificateStoreLoader certificateStoreLoader)
193+
{
194+
_certificateStoreLoader = certificateStoreLoader;
195+
}
196+
112197
public string Subject { get; set; }
113198
public string StoreName { get; set; }
114199
public string StoreLocation { get; set; }
@@ -121,52 +206,7 @@ public override X509Certificate2 Load()
121206
throw new InvalidOperationException($"Invalid store location: {StoreLocation}");
122207
}
123208

124-
using (var store = new X509Store(StoreName, storeLocation))
125-
{
126-
X509Certificate2Collection storeCertificates = null;
127-
X509Certificate2Collection foundCertificates = null;
128-
X509Certificate2 foundCertificate = null;
129-
130-
try
131-
{
132-
store.Open(OpenFlags.ReadOnly);
133-
storeCertificates = store.Certificates;
134-
foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectDistinguishedName, Subject, validOnly: !AllowInvalid);
135-
foundCertificate = foundCertificates
136-
.OfType<X509Certificate2>()
137-
.OrderByDescending(certificate => certificate.NotAfter)
138-
.FirstOrDefault();
139-
140-
if (foundCertificate == null)
141-
{
142-
throw new InvalidOperationException($"No certificate found for {Subject} in store {StoreName} in {StoreLocation}");
143-
}
144-
145-
return foundCertificate;
146-
}
147-
finally
148-
{
149-
if (foundCertificate != null)
150-
{
151-
storeCertificates.Remove(foundCertificate);
152-
foundCertificates.Remove(foundCertificate);
153-
}
154-
155-
DisposeCertificates(storeCertificates);
156-
DisposeCertificates(foundCertificates);
157-
}
158-
}
159-
}
160-
161-
private void DisposeCertificates(X509Certificate2Collection certificates)
162-
{
163-
if (certificates != null)
164-
{
165-
foreach (var certificate in certificates)
166-
{
167-
certificate.Dispose();
168-
}
169-
}
209+
return _certificateStoreLoader.Load(Subject, StoreName, storeLocation, !AllowInvalid);
170210
}
171211
}
172212
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq;
5+
using System.Security.Cryptography.X509Certificates;
6+
7+
namespace Microsoft.AspNetCore
8+
{
9+
internal class CertificateStoreLoader : ICertificateStoreLoader
10+
{
11+
public X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly)
12+
{
13+
using (var store = new X509Store(storeName, storeLocation))
14+
{
15+
X509Certificate2Collection storeCertificates = null;
16+
X509Certificate2Collection foundCertificates = null;
17+
X509Certificate2 foundCertificate = null;
18+
19+
try
20+
{
21+
store.Open(OpenFlags.ReadOnly);
22+
storeCertificates = store.Certificates;
23+
foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectDistinguishedName, subject, validOnly);
24+
foundCertificate = foundCertificates
25+
.OfType<X509Certificate2>()
26+
.OrderByDescending(certificate => certificate.NotAfter)
27+
.FirstOrDefault();
28+
29+
return foundCertificate;
30+
}
31+
finally
32+
{
33+
if (foundCertificate != null)
34+
{
35+
storeCertificates.Remove(foundCertificate);
36+
foundCertificates.Remove(foundCertificate);
37+
}
38+
39+
DisposeCertificates(storeCertificates);
40+
DisposeCertificates(foundCertificates);
41+
}
42+
}
43+
}
44+
45+
private void DisposeCertificates(X509Certificate2Collection certificates)
46+
{
47+
if (certificates != null)
48+
{
49+
foreach (var certificate in certificates)
50+
{
51+
certificate.Dispose();
52+
}
53+
}
54+
}
55+
}
56+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace Microsoft.AspNetCore
7+
{
8+
internal interface ICertificateFileLoader
9+
{
10+
X509Certificate2 Load(string path, string password, X509KeyStorageFlags flags);
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace Microsoft.AspNetCore
7+
{
8+
internal interface ICertificateStoreLoader
9+
{
10+
X509Certificate2 Load(string subject, string storeName, StoreLocation storeLocation, bool validOnly);
11+
}
12+
}

0 commit comments

Comments
 (0)