Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Templates] Generate single test HTTPS Certificate #57431

Merged
merged 11 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 3 additions & 1 deletion eng/Npm.Workspace.nodeproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

<Message Text="Building NPM packages..." Importance="high" />

<Exec Condition="'$(ContinuousIntegrationBuild)' == 'true'"
<Exec
Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --update-versions $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)"
EnvironmentVariables="$(_NpmAdditionalEnvironmentVariables)" />

Expand All @@ -58,6 +58,8 @@
</PropertyGroup>
<Message Text="Packing NPM packages..." Importance="high" />
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists('$(PackageOutputPath)')" />
<MakeDir Directories="$(IntermediateOutputPath)" Condition="!Exists('$(IntermediateOutputPath)')" />

<Exec
Command="node $(MSBuildThisFileDirectory)scripts/npm/pack-workspace.mjs --create-packages $(RepoRoot)package.json $(PackageVersion) $(PackageOutputPath) $(IntermediateOutputPath)"
EnvironmentVariables="$(_NpmAdditionalEnvironmentVariables)" />
Expand Down
74 changes: 74 additions & 0 deletions eng/tools/RepoTasks/GenerateTestDevCert.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace RepoTasks;

/// <summary>
/// Generates the test HTTPs certificate used by the template tests
/// </summary>
public class GenerateTestDevCert : Task
{
[Required]
public string CertificatePath { get; private set; }

public override bool Execute()
{
Mutex mutex = null;
try
{
// MSBuild will potentially invoke this task in parallel across different subprocesses/nodes.
// The build is configured to generate the certificate in a single location, but multiple projects
// import the same targets that will use this task, which will result in multiple calls.
// To avoid issues where we try to generate multiple certificates on the same location, we wrap the
// usage in a named mutex which guarantees that only one instance will run at a time.
mutex = new(initiallyOwned: true, "Global\\GenerateTestDevCert", out var createdNew);
if (!createdNew)
{
// The mutex already exists, wait for it to be released.
mutex.WaitOne();
}

if (File.Exists(CertificatePath))
{
Log.LogMessage(MessageImportance.Normal, $"A test certificate already exists at {CertificatePath}");
return true;
}

var cert = DevelopmentCertificate.Create(CertificatePath);

var devCertJsonFile = Path.ChangeExtension(CertificatePath, ".json");
var devCertJson = new CertificateInfo
{
Password = cert.CertificatePassword,
Thumbprint = cert.CertificateThumbprint
};

using var file = File.OpenWrite(devCertJsonFile);
file.SetLength(0);
JsonSerializer.Serialize(file, devCertJson);
}
catch (Exception e)
{
Log.LogErrorFromException(e, showStackTrace: true);
}
finally
{
mutex.ReleaseMutex();
}

return !Log.HasLoggedErrors;
}

private class CertificateInfo
{
public string Password { get; set; }
public string Thumbprint { get; set; }
}
}
10 changes: 10 additions & 0 deletions eng/tools/RepoTasks/RepoTasks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelVersion)" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net472'">
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" LinkBase="shared\CertificateGeneration" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<Compile Remove="GenerateTestDevCert.cs" />
<Compile Remove="shared\CertificateGeneration\*.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
<PackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkVersion)" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(MicrosoftBuildTasksCoreVersion)" />
Expand All @@ -46,4 +55,5 @@
<HintPath>$(WiXSdkPath)\Microsoft.Deployment.WindowsInstaller.Package.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions eng/tools/RepoTasks/RepoTasks.tasks
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
<UsingTask TaskName="RepoTasks.GenerateSharedFrameworkDepsFile" AssemblyFile="$(_RepoTaskAssembly)" />
<UsingTask TaskName="RepoTasks.CreateFrameworkListFile" AssemblyFile="$(_RepoTaskAssembly)" />
<UsingTask TaskName="RepoTasks.RemoveSharedFrameworkDependencies" AssemblyFile="$(_RepoTaskAssembly)" />
<UsingTask TaskName="RepoTasks.GenerateTestDevCert" AssemblyFile="$(_RepoTaskAssembly)" />
<UsingTask TaskName="DownloadFile" AssemblyFile="$(ArcadeSdkBuildTasksAssembly)" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Certificates.Generation;

namespace RepoTasks;

public readonly struct DevelopmentCertificate
{
public DevelopmentCertificate(string certificatePath, string certificatePassword, string certificateThumbprint)
{
CertificatePath = certificatePath;
CertificatePassword = certificatePassword;
CertificateThumbprint = certificateThumbprint;
}

public readonly string CertificatePath { get; }
public readonly string CertificatePassword { get; }
public readonly string CertificateThumbprint { get; }

public static DevelopmentCertificate Create(string certificatePath)
{
var certificatePassword = "";
var certificateThumbprint = EnsureDevelopmentCertificates(certificatePath, certificatePassword);

return new DevelopmentCertificate(certificatePath, certificatePassword, certificateThumbprint);
}

private static string EnsureDevelopmentCertificates(string certificatePath, string certificatePassword)
{
var now = DateTimeOffset.Now;
var manager = CertificateManager.Instance;
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
var certificateThumbprint = certificate.Thumbprint;
manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx);

return certificateThumbprint;
}
}
6 changes: 6 additions & 0 deletions eng/tools/RepoTasks/shared/CertificateGeneration/Global.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

global using System;
global using System.Collections.Generic;
global using System.IO;
69 changes: 44 additions & 25 deletions src/ProjectTemplates/Shared/DevelopmentCertificate.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,61 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using Microsoft.AspNetCore.Certificates.Generation;
using System.Reflection;
using System.Text.Json;

namespace Templates.Test.Helpers;

public readonly struct DevelopmentCertificate
public readonly struct DevelopmentCertificate(string certificatePath, string certificatePassword, string certificateThumbprint)
{
public DevelopmentCertificate(string certificatePath, string certificatePassword, string certificateThumbprint)
public readonly string CertificatePath { get; } = certificatePath;
public readonly string CertificatePassword { get; } = certificatePassword;
public readonly string CertificateThumbprint { get; } = certificateThumbprint;

public static DevelopmentCertificate Get(Assembly assembly)
{
CertificatePath = certificatePath;
CertificatePassword = certificatePassword;
CertificateThumbprint = certificateThumbprint;
}
string[] locations = [
Path.Combine(AppContext.BaseDirectory, "aspnetcore-https.json"),
Path.Combine(Environment.CurrentDirectory, "aspnetcore-https.json"),
Path.Combine(Path.GetDirectoryName(assembly.Location)!, "aspnetcore-https.json")
javiercn marked this conversation as resolved.
Show resolved Hide resolved
javiercn marked this conversation as resolved.
Show resolved Hide resolved
];

public readonly string CertificatePath { get; }
public readonly string CertificatePassword { get; }
public readonly string CertificateThumbprint { get; }
var json = TryGetExistingFile(locations)
?? throw new InvalidOperationException($"The aspnetcore-https.json file does not exist. Searched locations: {Environment.NewLine}{string.Join(Environment.NewLine, locations)}");

public static DevelopmentCertificate Create(string workingDirectory)
{
var certificatePath = Path.Combine(workingDirectory, $"{Guid.NewGuid()}.pfx");
var certificatePassword = Guid.NewGuid().ToString();
var certificateThumbprint = EnsureDevelopmentCertificates(certificatePath, certificatePassword);
using var file = File.OpenRead(json);
var certificateAttributes = JsonSerializer.Deserialize<CertificateAttributes>(file) ??
throw new InvalidOperationException($"The aspnetcore-https.json file does not contain valid JSON.");

var path = Path.ChangeExtension(json, ".pfx");

return new DevelopmentCertificate(certificatePath, certificatePassword, certificateThumbprint);
if (!File.Exists(path))
{
throw new InvalidOperationException($"The certificate file does not exist. Expected at: '{path}'.");
}

var password = certificateAttributes.Password;
var thumbprint = certificateAttributes.Thumbprint;

return new DevelopmentCertificate(path, password, thumbprint);
}

private static string EnsureDevelopmentCertificates(string certificatePath, string certificatePassword)
private static string TryGetExistingFile(string[] locations)
{
var now = DateTimeOffset.Now;
var manager = CertificateManager.Instance;
var certificate = manager.CreateAspNetCoreHttpsDevelopmentCertificate(now, now.AddYears(1));
var certificateThumbprint = certificate.Thumbprint;
manager.ExportCertificate(certificate, path: certificatePath, includePrivateKey: true, certificatePassword, CertificateKeyExportFormat.Pfx);
foreach (var location in locations)
{
if (File.Exists(location))
{
return location;
}
}

return null;
}

return certificateThumbprint;
private sealed class CertificateAttributes
{
public string Password { get; set; }
public string Thumbprint { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/ProjectTemplates/Shared/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static string ArtifactsLogDir
public string TemplateOutputDir { get; set; }
public string TargetFramework { get; set; } = GetAssemblyMetadata("Test.DefaultTargetFramework");
public string RuntimeIdentifier { get; set; } = string.Empty;
public static DevelopmentCertificate DevCert { get; } = DevelopmentCertificate.Create(AppContext.BaseDirectory);
public static DevelopmentCertificate DevCert { get; } = DevelopmentCertificate.Get(typeof(Project).Assembly);

public string TemplateBuildDir => Path.Combine(TemplateOutputDir, "bin", "Debug", TargetFramework, RuntimeIdentifier);
public string TemplatePublishDir => Path.Combine(TemplateOutputDir, "bin", "Release", TargetFramework, RuntimeIdentifier, "publish");
Expand Down
26 changes: 26 additions & 0 deletions src/ProjectTemplates/TestInfrastructure/PrepareForTest.targets
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@
</AssemblyAttribute>
</ItemGroup>

<Target Name="GenerateTesDevCert" BeforeTargets="AssignTargetPaths">

<PropertyGroup>
<_DevCertFileName>aspnetcore-https.pfx</_DevCertFileName>
<_DevCertJsonFileName>aspnetcore-https.json</_DevCertJsonFileName>
<_DevCertPath>$(ArtifactsTmpDir)$(_DevCertFileName)</_DevCertPath>
<_DevCertJsonPath>$(ArtifactsTmpDir)$(_DevCertJsonFileName)</_DevCertJsonPath>
<_DotNetHostFileName Condition="$([MSBuild]::IsOSPlatform(`Windows`))">dotnet.exe</_DotNetHostFileName>
javiercn marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>

<!-- This task only tries to generate a certificate if there is none existing at the location provided as path. -->
<GenerateTestDevCert
CertificatePath="$(_DevCertPath)"
Condition="'$(MSBuildRuntimeType)' == 'core'">
</GenerateTestDevCert>

<Error Condition="!Exists('$(_DevCertPath)') and '$(MSBuildRuntimeType)' == 'core'" Text="Failed to generate a test certificate at $(_DevCertPath)" />
<Error Condition="!Exists('$(_DevCertPath)') and '$(MSBuildRuntimeType)' != 'core'" Text="No dev cert exists at $(_DevCertPath). Ensure you follow the instructions from src/ProjectTemplates/README.md before running inside Visual Studio" />

<ItemGroup>
<Content Include="$(_DevCertPath)" CopyToPublishDirectory="PreserveNewest" />
<Content Include="$(_DevCertJsonPath)" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

</Target>

<Target Name="PrepareForTest" BeforeTargets="GetAssemblyAttributes" Condition=" '$(DesignTimeBuild)' != 'true' ">
<PropertyGroup>
<TestTemplateCreationFolder>$([MSBuild]::NormalizePath('$(OutputPath)$(TestTemplateCreationFolder)'))</TestTemplateCreationFolder>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<!-- Shared testing infrastructure for running E2E tests using selenium -->
<Import Condition="'$(SkipTestBuild)' != 'true'" Project="$(SharedSourceRoot)E2ETesting\E2ETesting.props" />

Expand Down
Loading