diff --git a/DotNetTools.sln b/DotNetTools.sln
index e788eb2226..8af718e399 100644
--- a/DotNetTools.sln
+++ b/DotNetTools.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26210.0
+VisualStudioVersion = 15.0.26510.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}"
EndProject
@@ -24,6 +24,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Watcher.To
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Caching.SqlConfig.Tools", "src\Microsoft.Extensions.Caching.SqlConfig.Tools\Microsoft.Extensions.Caching.SqlConfig.Tools.csproj", "{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.CertificateGeneration.Task", "src\Microsoft.AspNetCore.CertificateGeneration.Task\Microsoft.AspNetCore.CertificateGeneration.Task.csproj", "{7B293291-26F4-47F0-9C2F-E396F35A4280}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.CertificateGeneration.Task.Tests", "test\Microsoft.AspNetcore.CertificateGeneration.Task.Tests\Microsoft.AspNetCore.CertificateGeneration.Task.Tests.csproj", "{3A7EF01A-073B-4123-850D-DFA4701EBE5B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +58,14 @@ Global
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7B293291-26F4-47F0-9C2F-E396F35A4280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7B293291-26F4-47F0-9C2F-E396F35A4280}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7B293291-26F4-47F0-9C2F-E396F35A4280}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7B293291-26F4-47F0-9C2F-E396F35A4280}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3A7EF01A-073B-4123-850D-DFA4701EBE5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3A7EF01A-073B-4123-850D-DFA4701EBE5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3A7EF01A-073B-4123-850D-DFA4701EBE5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3A7EF01A-073B-4123-850D-DFA4701EBE5B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -65,5 +77,7 @@ Global
{7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4}
+ {7B293291-26F4-47F0-9C2F-E396F35A4280} = {66517987-2A5A-4330-B130-207039378FD4}
+ {3A7EF01A-073B-4123-850D-DFA4701EBE5B} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
EndGlobalSection
EndGlobal
diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json
index 92dad6ebe4..214ca77a9c 100644
--- a/NuGetPackageVerifier.json
+++ b/NuGetPackageVerifier.json
@@ -18,6 +18,13 @@
"packageTypes": [
"DotnetCliTool"
]
+ },
+ "Microsoft.AspNetCore.CertificateGeneration.Task": {
+ "exclusions":{
+ "BUILD_ITEMS_FRAMEWORK": {
+ "*": "This is an MSBuild task intended to run through dotnet msbuild /t:Target independently of whether your project targets full framework or .net core."
+ }
+ }
}
}
},
diff --git a/build/dependencies.props b/build/dependencies.props
index afd45bf3ab..a6c3217f9f 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -11,6 +11,7 @@
4.3.0
2.1.0-*
+ 15.1.1012
$(BundledNETStandardPackageVersion)
15.3.0-*
2.3.0-beta2-*
diff --git a/src/Microsoft.AspNetCore.CertificateGeneration.Task/CertificateManager.cs b/src/Microsoft.AspNetCore.CertificateGeneration.Task/CertificateManager.cs
new file mode 100644
index 0000000000..e53c6b8743
--- /dev/null
+++ b/src/Microsoft.AspNetCore.CertificateGeneration.Task/CertificateManager.cs
@@ -0,0 +1,96 @@
+// 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.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Microsoft.AspNetCore.CertificateGeneration.Task
+{
+ internal static class CertificateManager
+ {
+ public static X509Certificate2 GenerateSSLCertificate(
+ string subjectName,
+ IEnumerable subjectAlternativeName,
+ string friendlyName,
+ DateTimeOffset notBefore,
+ DateTimeOffset expires,
+ StoreName storeName,
+ StoreLocation storeLocation)
+ {
+ using (var rsa = RSA.Create(2048))
+ {
+ var signingRequest = new CertificateRequest(
+ new X500DistinguishedName(subjectName), rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ var enhancedKeyUsage = new OidCollection();
+ enhancedKeyUsage.Add(new Oid("1.3.6.1.5.5.7.3.1", "Server Authentication"));
+ signingRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(enhancedKeyUsage, critical: true));
+ signingRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true));
+ signingRequest.CertificateExtensions.Add(
+ new X509BasicConstraintsExtension(
+ certificateAuthority: false,
+ hasPathLengthConstraint: false,
+ pathLengthConstraint: 0,
+ critical: true));
+
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ foreach (var alternativeName in subjectAlternativeName)
+ {
+ sanBuilder.AddDnsName(alternativeName);
+ }
+ signingRequest.CertificateExtensions.Add(sanBuilder.Build());
+
+ var certificate = signingRequest.CreateSelfSigned(notBefore, expires);
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ certificate.FriendlyName = friendlyName;
+ }
+
+ SaveCertificate(storeName, storeLocation, certificate);
+
+ return certificate;
+ }
+ }
+
+ private static void SaveCertificate(StoreName storeName, StoreLocation storeLocation, X509Certificate2 certificate)
+ {
+ // We need to take this step so that the key gets persisted.
+ var imported = certificate;
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ var export = certificate.Export(X509ContentType.Pkcs12, "");
+ imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet);
+ Array.Clear(export, 0, export.Length);
+ }
+
+ using (var store = new X509Store(storeName, storeLocation))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(imported);
+ store.Close();
+ };
+ }
+
+ public static X509Certificate2 FindCertificate(string subjectValue, StoreName storeName, StoreLocation storeLocation)
+ {
+ using (var store = new X509Store(storeName, storeLocation))
+ {
+ store.Open(OpenFlags.ReadOnly);
+ var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, subjectValue, validOnly: false);
+ var current = DateTimeOffset.UtcNow;
+
+ var found = certificates.OfType()
+ .Where(c => c.NotBefore <= current && current <= c.NotAfter && c.HasPrivateKey)
+ .FirstOrDefault();
+ store.Close();
+
+ return found;
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.CertificateGeneration.Task/GenerateSSLCertificateTask.cs b/src/Microsoft.AspNetCore.CertificateGeneration.Task/GenerateSSLCertificateTask.cs
new file mode 100644
index 0000000000..30ccf8bacb
--- /dev/null
+++ b/src/Microsoft.AspNetCore.CertificateGeneration.Task/GenerateSSLCertificateTask.cs
@@ -0,0 +1,43 @@
+// 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.Security.Cryptography.X509Certificates;
+using Microsoft.Build.Framework;
+
+namespace Microsoft.AspNetCore.CertificateGeneration.Task
+{
+ public class GenerateSSLCertificateTask : Build.Utilities.Task
+ {
+ public bool Force { get; set; }
+
+ protected string Subject { get; set; } = "CN=localhost";
+
+ public override bool Execute()
+ {
+ var subjectValue = Subject;
+ var sansValue = new List { "localhost" };
+ var friendlyNameValue = "ASP.NET Core HTTPS development certificate";
+ var notBeforeValue = DateTime.UtcNow;
+ var expiresValue = DateTime.UtcNow.AddYears(1);
+ var storeNameValue = StoreName.My;
+ var storeLocationValue = StoreLocation.CurrentUser;
+
+ var cert = CertificateManager.FindCertificate(subjectValue, storeNameValue, storeLocationValue);
+
+ if (cert != null && !Force)
+ {
+ LogMessage($"A certificate with subject name '{Subject}' already exists. Skipping certificate generation.");
+ return true;
+ }
+
+ var generated = CertificateManager.GenerateSSLCertificate(subjectValue, sansValue, friendlyNameValue, notBeforeValue, expiresValue, storeNameValue, storeLocationValue);
+ LogMessage($"Generated certificate {generated.SubjectName.Name} - {generated.Thumbprint} - {generated.FriendlyName}");
+
+ return true;
+ }
+
+ protected virtual void LogMessage(string message) => Log.LogMessage(MessageImportance.High, message);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.CertificateGeneration.Task/Microsoft.AspNetCore.CertificateGeneration.Task.csproj b/src/Microsoft.AspNetCore.CertificateGeneration.Task/Microsoft.AspNetCore.CertificateGeneration.Task.csproj
new file mode 100644
index 0000000000..59e932ab97
--- /dev/null
+++ b/src/Microsoft.AspNetCore.CertificateGeneration.Task/Microsoft.AspNetCore.CertificateGeneration.Task.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+ netcoreapp2.0
+ MSBuild target for generating HTTPS certificates for development cross-platform.
+ tools
+ asp.net;ssl;certificates
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AspNetCore.CertificateGeneration.Task/README.md b/src/Microsoft.AspNetCore.CertificateGeneration.Task/README.md
new file mode 100644
index 0000000000..4dfea325ab
--- /dev/null
+++ b/src/Microsoft.AspNetCore.CertificateGeneration.Task/README.md
@@ -0,0 +1,48 @@
+Microsoft.AspNetCore.CertificateGeneration.Task
+===============================================
+Microsoft.AspNetCore.CertificateGeneration.Task is an MSBuild task to generate SSL certificates
+for use in ASP.NET Core for development purposes.
+
+### How To Install
+
+Install `Microsoft.AspNetCore.CertificateGeneration.Task` as a `PackageReference` to your project.
+
+```xml
+
+
+
+```
+
+### How To Use
+
+The command must be executed in the directory that contains the project with the reference to the package.
+
+ Usage: dotnet msbuild /t:GenerateSSLCertificate [/p:ForceGenerateSSLCertificate=true]
+
+### Testing scenarios
+
+On a machine without an SSL certificate generated by this task. Create a netcoreapp2.0 mvc application using
+
+```
+dotnet new mvc
+```
+
+Then try to run the app using:
+
+```
+dotnet run
+```
+
+When the application fails to run due to a missing SSL certificate. Run:
+
+```
+dotnet msbuild /t:GenerateSSLCertificate
+```
+
+Run the application again using:
+
+```
+dotnet run
+```
+
+The application should run successfully. You will still have to trust the certificate as a separate step.
diff --git a/src/Microsoft.AspNetCore.CertificateGeneration.Task/build/Microsoft.AspNetCore.CertificateGeneration.Task.targets b/src/Microsoft.AspNetCore.CertificateGeneration.Task/build/Microsoft.AspNetCore.CertificateGeneration.Task.targets
new file mode 100644
index 0000000000..1deeb55dbb
--- /dev/null
+++ b/src/Microsoft.AspNetCore.CertificateGeneration.Task/build/Microsoft.AspNetCore.CertificateGeneration.Task.targets
@@ -0,0 +1,20 @@
+
+
+
+
+ <_SSLCertificateGenerationTaskAssembly>$(MSBuildThisFileDirectory)..\tools\netcoreapp2.0\Microsoft.AspNetCore.CertificateGeneration.Task.dll
+
+
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNetcore.CertificateGeneration.Task.Tests/GenerateSSLCertificateTaskTest.cs b/test/Microsoft.AspNetcore.CertificateGeneration.Task.Tests/GenerateSSLCertificateTaskTest.cs
new file mode 100644
index 0000000000..245333a84e
--- /dev/null
+++ b/test/Microsoft.AspNetcore.CertificateGeneration.Task.Tests/GenerateSSLCertificateTaskTest.cs
@@ -0,0 +1,227 @@
+// 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.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using Xunit;
+
+namespace Microsoft.AspNetCore.CertificateGeneration.Task
+{
+ public class GenerateSSLCertificateTaskTest : IDisposable
+ {
+ private const string TestSubject = "CN=test.ssl.localhost";
+
+ [Fact]
+ public void GenerateSSLCertificateTaskTest_CreatesCertificate_IfNoCertificateIsFound()
+ {
+ // Arrange
+ EnsureCleanUp();
+ var task = new TestGenerateSSLCertificateTask();
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.True(result);
+ var certificates = GetTestCertificates();
+ Assert.Equal(1, certificates.Count);
+ Assert.Equal(1, task.Messages.Count);
+ Assert.StartsWith($"Generated certificate {TestSubject}", task.Messages[0]);
+ }
+
+ [Fact]
+ public void GenerateSSLCertificateTaskTest_CreatesCertificate_IfFoundCertificateHasExpired()
+ {
+ // Arrange
+ EnsureCleanUp();
+ CreateCertificate(notBefore: DateTimeOffset.UtcNow.AddYears(-2), expires: DateTimeOffset.UtcNow.AddYears(-1));
+
+ var task = new TestGenerateSSLCertificateTask();
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.True(result);
+ var certificates = GetTestCertificates();
+ Assert.Equal(2, certificates.Count);
+ Assert.Equal(1, task.Messages.Count);
+ Assert.StartsWith($"Generated certificate {TestSubject}", task.Messages[0]);
+ }
+
+ [Fact]
+ public void GenerateSSLCertificateTaskTest_CreatesCertificate_IfFoundCertificateIsNotYetValid()
+ {
+ // Arrange
+ EnsureCleanUp();
+ CreateCertificate(notBefore: DateTimeOffset.UtcNow.AddYears(1), expires: DateTimeOffset.UtcNow.AddYears(2));
+
+ var task = new TestGenerateSSLCertificateTask();
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.True(result);
+ var certificates = GetTestCertificates();
+ Assert.Equal(2, certificates.Count);
+ Assert.Equal(1, task.Messages.Count);
+ Assert.StartsWith($"Generated certificate {TestSubject}", task.Messages[0]);
+ }
+
+ [Fact]
+ public void GenerateSSLCertificateTaskTest_CreatesCertificate_IfFoundCertificateDoesNotHavePrivateKeys()
+ {
+ // Arrange
+ EnsureCleanUp();
+ CreateCertificate(savePrivateKey: false);
+ var task = new TestGenerateSSLCertificateTask();
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.True(result);
+ var certificates = GetTestCertificates();
+ Assert.Equal(2, certificates.Count);
+ Assert.Equal(1, task.Messages.Count);
+ Assert.StartsWith($"Generated certificate {TestSubject}", task.Messages[0]);
+ }
+
+ [Fact]
+ public void GenerateSSLCertificateTaskTest_DoesNothing_IfValidCertificateIsFound()
+ {
+ // Arrange
+ EnsureCleanUp();
+ CreateCertificate();
+ var task = new TestGenerateSSLCertificateTask();
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.True(result);
+ var certificates = GetTestCertificates();
+ Assert.Equal(1, certificates.Count);
+ Assert.Equal(1, task.Messages.Count);
+ Assert.Equal($"A certificate with subject name '{TestSubject}' already exists. Skipping certificate generation.", task.Messages[0]);
+ }
+
+ [Fact]
+ public void GenerateSSLCertificateTaskTest_CreatesACertificateWhenThereIsAlreadyAValidCertificate_IfForceIsSpecified()
+ {
+ // Arrange
+ EnsureCleanUp();
+ CreateCertificate();
+ var task = new TestGenerateSSLCertificateTask() { Force = true };
+
+ // Act
+ var result = task.Execute();
+
+ // Assert
+ Assert.True(result);
+ var certificates = GetTestCertificates();
+ Assert.Equal(2, certificates.Count);
+ Assert.Equal(1, task.Messages.Count);
+ Assert.StartsWith($"Generated certificate {TestSubject}", task.Messages[0]);
+ }
+
+ public X509CertificateCollection GetTestCertificates()
+ {
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, TestSubject, validOnly: false);
+ store.Close();
+
+ return certificates;
+ }
+ }
+
+ private void EnsureCleanUp()
+ {
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ var certificates = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, TestSubject, validOnly: false);
+ store.RemoveRange(certificates);
+ store.Close();
+ }
+ }
+
+ public void Dispose()
+ {
+ EnsureCleanUp();
+ }
+
+ private void CreateCertificate(
+ DateTimeOffset notBefore = default(DateTimeOffset),
+ DateTimeOffset expires = default(DateTimeOffset),
+ bool savePrivateKey = true)
+ {
+ using (var rsa = RSA.Create(2048))
+ {
+ var signingRequest = new CertificateRequest(
+ new X500DistinguishedName(TestSubject), rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ var enhancedKeyUsage = new OidCollection();
+ enhancedKeyUsage.Add(new Oid("1.3.6.1.5.5.7.3.1", "Server Authentication"));
+ signingRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(enhancedKeyUsage, critical: true));
+ signingRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyEncipherment, critical: true));
+ signingRequest.CertificateExtensions.Add(
+ new X509BasicConstraintsExtension(
+ certificateAuthority: false,
+ hasPathLengthConstraint: false,
+ pathLengthConstraint: 0,
+ critical: true));
+
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ sanBuilder.AddDnsName(TestSubject.Replace("CN=", ""));
+ signingRequest.CertificateExtensions.Add(sanBuilder.Build());
+
+ var certificate = signingRequest.CreateSelfSigned(
+ notBefore == default(DateTimeOffset) ? DateTimeOffset.Now : notBefore,
+ expires == default(DateTimeOffset) ? DateTimeOffset.Now.AddYears(1) : expires);
+
+
+ var imported = certificate;
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && savePrivateKey)
+ {
+ var export = certificate.Export(X509ContentType.Pkcs12, "");
+
+ imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet);
+ Array.Clear(export, 0, export.Length);
+ }
+ else if (!savePrivateKey)
+ {
+ var export = certificate.Export(X509ContentType.Cert, "");
+
+ imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet);
+ Array.Clear(export, 0, export.Length);
+ }
+
+ using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(imported);
+ store.Close();
+ };
+ }
+ }
+
+ private class TestGenerateSSLCertificateTask : GenerateSSLCertificateTask
+ {
+ public TestGenerateSSLCertificateTask()
+ {
+ Subject = TestSubject;
+ }
+
+ public IList Messages { get; set; } = new List();
+
+ protected override void LogMessage(string message) => Messages.Add(message);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetcore.CertificateGeneration.Task.Tests/Microsoft.AspNetCore.CertificateGeneration.Task.Tests.csproj b/test/Microsoft.AspNetcore.CertificateGeneration.Task.Tests/Microsoft.AspNetCore.CertificateGeneration.Task.Tests.csproj
new file mode 100644
index 0000000000..6e956b40f5
--- /dev/null
+++ b/test/Microsoft.AspNetcore.CertificateGeneration.Task.Tests/Microsoft.AspNetCore.CertificateGeneration.Task.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+ netcoreapp2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+