Add HTTPS developer certificate management tool
This commit is contained in:
parent
c4788107f5
commit
ff0f112d7b
|
|
@ -1,6 +1,6 @@
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.26815.3
|
VisualStudioVersion = 15.0.26927.1
|
||||||
MinimumVisualStudioVersion = 15.0.26730.03
|
MinimumVisualStudioVersion = 15.0.26730.03
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
|
@ -44,9 +44,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Watcher.To
|
||||||
EndProject
|
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}"
|
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
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.DeveloperCertificates.Tools", "src\Microsoft.AspNetCore.DeveloperCertificates.Tools\Microsoft.AspNetCore.DeveloperCertificates.Tools.csproj", "{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}"
|
||||||
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
@ -78,14 +76,10 @@ Global
|
||||||
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = 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
|
{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7B293291-26F4-47F0-9C2F-E396F35A4280}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7B293291-26F4-47F0-9C2F-E396F35A4280}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7B293291-26F4-47F0-9C2F-E396F35A4280}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -97,8 +91,7 @@ Global
|
||||||
{7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
|
{7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
|
||||||
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {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}
|
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4}
|
||||||
{7B293291-26F4-47F0-9C2F-E396F35A4280} = {66517987-2A5A-4330-B130-207039378FD4}
|
{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8} = {66517987-2A5A-4330-B130-207039378FD4}
|
||||||
{3A7EF01A-073B-4123-850D-DFA4701EBE5B} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {57C07F14-2EAC-44FF-A277-B9221B4B2BF7}
|
SolutionGuid = {57C07F14-2EAC-44FF-A277-B9221B4B2BF7}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,10 @@
|
||||||
"DotnetCliTool"
|
"DotnetCliTool"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Microsoft.AspNetCore.CertificateGeneration.Task": {
|
"Microsoft.AspNetCore.DeveloperCertificates.Tools": {
|
||||||
"exclusions":{
|
"packageTypes": [
|
||||||
"BUILD_ITEMS_FRAMEWORK": {
|
"DotnetCliTool"
|
||||||
"*": "This is an MSBuild task intended to run through dotnet msbuild /t:Target independently of whether your project targets full framework or .net core."
|
]
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
// 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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<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.Utilities.Core" PrivateAssets="All" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
|
<AssemblyName>dotnet-developercertificates</AssemblyName>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<Description>Command line tool to generate certificates used in ASP.NET Core during development.</Description>
|
||||||
|
<PackageId>Microsoft.AspNetCore.DeveloperCertificates.Tools</PackageId>
|
||||||
|
<PackageTags>dotnet;developercertificates</PackageTags>
|
||||||
|
<PackageType>DotnetCliTool</PackageType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\shared\CliContext.cs" Link="CliContext.cs" />
|
||||||
|
<Compile Include="..\..\shared\CommandLineApplicationExtensions.cs" Link="CommandLineApplicationExtensions.cs" />
|
||||||
|
<Compile Include="..\..\shared\ConsoleReporter.cs" Link="ConsoleReporter.cs" />
|
||||||
|
<Compile Include="..\..\shared\DebugHelper.cs" Link="DebugHelper.cs" />
|
||||||
|
<Compile Include="..\..\shared\Ensure.cs" Link="Ensure.cs" />
|
||||||
|
<Compile Include="..\..\shared\IConsole.cs" Link="IConsole.cs" />
|
||||||
|
<Compile Include="..\..\shared\IReporter.cs" Link="IReporter.cs" />
|
||||||
|
<Compile Include="..\..\shared\NullReporter.cs" Link="NullReporter.cs" />
|
||||||
|
<Compile Include="..\..\shared\PhysicalConsole.cs" Link="PhysicalConsole.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="prefercliruntime" PackagePath="\prefercliruntime" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Cng" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.Certificates.Generation;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.DeveloperCertificates.Tools
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
private const int CriticalError = -1;
|
||||||
|
private const int Success = 0;
|
||||||
|
private const int ErrorCreatingTheCertificate = 1;
|
||||||
|
private const int ErrorSavingTheCertificate = 2;
|
||||||
|
private const int ErrorExportingTheCertificate = 3;
|
||||||
|
private const int ErrorTrustingTheCertificate = 4;
|
||||||
|
|
||||||
|
public static readonly TimeSpan HttpsCertificateValidity = TimeSpan.FromDays(365);
|
||||||
|
|
||||||
|
public static int Main(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication
|
||||||
|
{
|
||||||
|
Name = "dotnet-developercertificates"
|
||||||
|
};
|
||||||
|
|
||||||
|
app.Command("https", c =>
|
||||||
|
{
|
||||||
|
var exportPath = c.Option("-ep|--export-path",
|
||||||
|
"Full path to the exported certificate",
|
||||||
|
CommandOptionType.SingleValue);
|
||||||
|
|
||||||
|
var password = c.Option("-p|--password",
|
||||||
|
"Password to use when exporting the certificate with the private key into a pfx file",
|
||||||
|
CommandOptionType.SingleValue);
|
||||||
|
|
||||||
|
CommandOption trust = null;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
trust = c.Option("-t|--trust",
|
||||||
|
"Trust the certificate on the current platform",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var verbose = c.Option("-v|--verbose",
|
||||||
|
"Display more debug information.",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
var quiet = c.Option("-q|--quiet",
|
||||||
|
"Display warnings and errors only.",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
|
c.HelpOption("-h|--help");
|
||||||
|
|
||||||
|
c.OnExecute(() =>
|
||||||
|
{
|
||||||
|
var reporter = new ConsoleReporter(PhysicalConsole.Singleton, verbose.HasValue(), quiet.HasValue());
|
||||||
|
return EnsureHttpsCertificate(exportPath, password, trust, reporter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.HelpOption("-h|--help");
|
||||||
|
|
||||||
|
app.OnExecute(() =>
|
||||||
|
{
|
||||||
|
app.ShowHelp();
|
||||||
|
return Success;
|
||||||
|
});
|
||||||
|
|
||||||
|
return app.Execute(args);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return CriticalError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int EnsureHttpsCertificate(CommandOption exportPath, CommandOption password, CommandOption trust, IReporter reporter)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
var manager = new CertificateManager();
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && trust?.HasValue() == true)
|
||||||
|
{
|
||||||
|
reporter.Warn("Trusting the HTTPS development certificate was requested. If the certificate is not " +
|
||||||
|
"already trusted we will run the following command:" + Environment.NewLine +
|
||||||
|
"'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <<certificate>>'" +
|
||||||
|
Environment.NewLine + "This command might prompt you for your password to install the certificate " +
|
||||||
|
"on the system keychain.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = manager.EnsureAspNetCoreHttpsDevelopmentCertificate(
|
||||||
|
now,
|
||||||
|
now.Add(HttpsCertificateValidity),
|
||||||
|
exportPath.Value(),
|
||||||
|
trust == null ? false : trust.HasValue(),
|
||||||
|
password.HasValue(),
|
||||||
|
password.Value());
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case EnsureCertificateResult.Succeeded:
|
||||||
|
reporter.Output("The HTTPS developer certificate was generated successfully.");
|
||||||
|
if (exportPath.Value() != null)
|
||||||
|
{
|
||||||
|
reporter.Verbose($"The certificate was exported to {Path.GetFullPath(exportPath.Value())}");
|
||||||
|
}
|
||||||
|
return Success;
|
||||||
|
case EnsureCertificateResult.ValidCertificatePresent:
|
||||||
|
reporter.Output("A valid HTTPS certificate is already present.");
|
||||||
|
if (exportPath.Value() != null)
|
||||||
|
{
|
||||||
|
reporter.Verbose($"The certificate was exported to {Path.GetFullPath(exportPath.Value())}");
|
||||||
|
}
|
||||||
|
return Success;
|
||||||
|
case EnsureCertificateResult.ErrorCreatingTheCertificate:
|
||||||
|
reporter.Error("There was an error creating the HTTPS developer certificate.");
|
||||||
|
return ErrorCreatingTheCertificate;
|
||||||
|
case EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore:
|
||||||
|
reporter.Error("There was an error saving the HTTPS developer certificate to the current user personal certificate store.");
|
||||||
|
return ErrorSavingTheCertificate;
|
||||||
|
case EnsureCertificateResult.ErrorExportingTheCertificate:
|
||||||
|
reporter.Warn("There was an error exporting HTTPS developer certificate to a file.");
|
||||||
|
return ErrorExportingTheCertificate;
|
||||||
|
case EnsureCertificateResult.FailedToTrustTheCertificate:
|
||||||
|
reporter.Warn("There was an error trusting HTTPS developer certificate.");
|
||||||
|
return ErrorTrustingTheCertificate;
|
||||||
|
default:
|
||||||
|
reporter.Error("Something went wrong. The HTTPS developer certificate could not be created.");
|
||||||
|
return CriticalError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
// 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.Single(certificates);
|
|
||||||
Assert.Single(task.Messages);
|
|
||||||
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.Single(task.Messages);
|
|
||||||
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.Single(task.Messages);
|
|
||||||
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.Single(certificates);
|
|
||||||
Assert.Single(task.Messages);
|
|
||||||
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.Single(task.Messages);
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.CertificateGeneration.Task\Microsoft.AspNetCore.CertificateGeneration.Task.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Build.Utilities.Core" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
Loading…
Reference in New Issue