Add task for generating ssl certificates with MSBuild

This commit is contained in:
Javier Calvarro Nelson 2017-05-22 10:03:19 -07:00
parent 0e3d091fb7
commit 7ac72dd7d8
10 changed files with 498 additions and 1 deletions

View File

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

View File

@ -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."
}
}
}
}
},

View File

@ -11,6 +11,7 @@
<CoreFxVersion>4.3.0</CoreFxVersion>
<InternalAspNetCoreSdkVersion>2.1.0-*</InternalAspNetCoreSdkVersion>
<MsBuildPackageVersions>15.1.1012</MsBuildPackageVersions>
<NETStandardImplicitPackageVersion>$(BundledNETStandardPackageVersion)</NETStandardImplicitPackageVersion>
<TestSdkVersion>15.3.0-*</TestSdkVersion>
<XunitVersion>2.3.0-beta2-*</XunitVersion>

View File

@ -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<string> 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<X509Certificate2>()
.Where(c => c.NotBefore <= current && current <= c.NotAfter && c.HasPrivateKey)
.FirstOrDefault();
store.Close();
return found;
};
}
}
}

View File

@ -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<string> { "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);
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Description>MSBuild target for generating HTTPS certificates for development cross-platform.</Description>
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<PackageTags>asp.net;ssl;certificates</PackageTags>
</PropertyGroup>
<ItemGroup>
<Content Include="build\**\*.targets" Pack="true" PackagePath="%(Identity)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(MsBuildPackageVersions)" />
</ItemGroup>
</Project>

View File

@ -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
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.CertificateGeneration.Task" Version="2.0.0" />
</ItemGroup>
```
### 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.

View File

@ -0,0 +1,20 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
********************************************************************************************
Target: GenerateSSLCertificate
Generates an SSL certificate to use for development in ASP.NET Core applications
********************************************************************************************
-->
<PropertyGroup>
<_SSLCertificateGenerationTaskAssembly>$(MSBuildThisFileDirectory)..\tools\netcoreapp2.0\Microsoft.AspNetCore.CertificateGeneration.Task.dll</_SSLCertificateGenerationTaskAssembly>
</PropertyGroup>
<UsingTask TaskName="Microsoft.AspNetCore.CertificateGeneration.Task.GenerateSSLCertificateTask"
AssemblyFile="$(_SSLCertificateGenerationTaskAssembly)" />
<Target Name="GenerateSSLCertificate">
<GenerateSSLCertificateTask Force="$(ForceGenerateSSLCertificate)" />
</Target>
</Project>

View File

@ -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<string> Messages { get; set; } = new List<string>();
protected override void LogMessage(string message) => Messages.Add(message);
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.CertificateGeneration.Task\Microsoft.AspNetCore.CertificateGeneration.Task.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
</ItemGroup>
</Project>