Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ Copyright (c) .NET Foundation. All rights reserved.
Condition="'$(UseAppHost)' == 'true' and '$(_IsExecutable)' == 'true'">
<PropertyGroup>
<_UseWindowsGraphicalUserInterface Condition="($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win'))) and '$(OutputType)'=='WinExe'">true</_UseWindowsGraphicalUserInterface>
<_EnableMacOSCodeSign Condition="'$(_EnableMacOSCodeSign)' == '' and $([MSBuild]::IsOSPlatform(`OSX`)) and Exists('/usr/bin/codesign') and
<_EnableMacOSCodeSign Condition="'$(_EnableMacOSCodeSign)' == '' and
($(RuntimeIdentifier.StartsWith('osx')) or $(AppHostRuntimeIdentifier.StartsWith('osx')))">true</_EnableMacOSCodeSign>
<_UseSingleFileHostForPublish Condition="'$(PublishSingleFile)' == 'true' and
'$(SelfContained)' == 'true' and
Expand Down
137 changes: 85 additions & 52 deletions test/Microsoft.NET.Build.Tests/AppHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable disable

using System.Buffers.Binary;
using System.Diagnostics;
using System.Reflection.PortableExecutable;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -152,71 +153,46 @@ public void It_does_not_try_to_codesign_non_osx_app_hosts(string targetFramework
Directory.Delete(buildProjDir, true);
}

[PlatformSpecificTheory(TestPlatforms.OSX)]
[InlineData("netcoreapp3.1")]
[InlineData("net5.0")]
[InlineData(ToolsetInfo.CurrentTargetFramework)]
public void It_codesigns_a_framework_dependent_app(string targetFramework)
{
var testAsset = _testAssetsManager
.CopyTestAsset("HelloWorld", identifier: targetFramework)
.WithSource()
.WithTargetFramework(targetFramework);

var buildCommand = new BuildCommand(testAsset);
buildCommand
.Execute()
.Should()
.Pass();

var outputDirectory = buildCommand.GetOutputDirectory(targetFramework);
var appHostFullPath = Path.Combine(outputDirectory.FullName, "HelloWorld");

// Check that the apphost is signed
var codesignPath = @"/usr/bin/codesign";
new RunExeCommand(Log, codesignPath, new string[] { "-s", "-", appHostFullPath })
.Execute()
.Should()
.Fail()
.And
.HaveStdErrContaining($"{appHostFullPath}: is already signed");
}

[PlatformSpecificTheory(TestPlatforms.OSX)]
[InlineData("netcoreapp3.1", false)]
[InlineData("netcoreapp3.1", true)]
[InlineData("net5.0", false)]
[InlineData("net5.0", true)]
[InlineData(ToolsetInfo.CurrentTargetFramework, false)]
[InlineData(ToolsetInfo.CurrentTargetFramework, true)]
public void It_codesigns_an_app_targeting_osx(string targetFramework, bool selfContained)
[Theory]
[InlineData("net6.0", "osx-x64")]
[InlineData("net6.0", "osx-arm64")]
[InlineData(ToolsetInfo.CurrentTargetFramework, "osx-x64")]
[InlineData(ToolsetInfo.CurrentTargetFramework, "osx-arm64")]
public void It_codesigns_an_app_targeting_osx(string targetFramework, string rid)
{
const string testAssetName = "HelloWorld";
var testAsset = _testAssetsManager
.CopyTestAsset("HelloWorld", identifier: targetFramework, allowCopyIfPresent: true)
.CopyTestAsset(testAssetName, identifier: targetFramework)
.WithSource()
.WithTargetFramework(targetFramework);

var buildCommand = new BuildCommand(testAsset);
var buildArgs = new List<string>() { $"/p:RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}" };
if (!selfContained)
buildArgs.Add("/p:PublishSingleFile=true");

var buildArgs = new List<string>() { $"/p:RuntimeIdentifier={rid}" };
buildCommand
.Execute(buildArgs.ToArray())
.Should()
.Pass();

var outputDirectory = buildCommand.GetOutputDirectory(targetFramework, runtimeIdentifier: RuntimeInformation.RuntimeIdentifier);
var appHostFullPath = Path.Combine(outputDirectory.FullName, "HelloWorld");
var outputDirectory = buildCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid);
var appHostFullPath = Path.Combine(outputDirectory.FullName, testAssetName);

// Check that the apphost is signed
var codesignPath = @"/usr/bin/codesign";
new RunExeCommand(Log, codesignPath, new string[] { "-s", "-", appHostFullPath })
.Execute()
.Should()
.Fail()
.And
.HaveStdErrContaining($"{appHostFullPath}: is already signed");
HasMachOSignatureLoadCommand(new FileInfo(appHostFullPath)).Should().BeTrue();
// When on a Mac, use the codesign tool to verify the signature as well
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
var codesignPath = @"/usr/bin/codesign";
new RunExeCommand(Log, codesignPath, ["-s", "-", appHostFullPath])
.Execute()
.Should()
.Fail()
.And
.HaveStdErrContaining($"{appHostFullPath}: is already signed");
new RunExeCommand(Log, codesignPath, ["-v", appHostFullPath])
.Execute()
.Should()
.Pass();
}
}

[Theory]
Expand Down Expand Up @@ -511,5 +487,62 @@ private static bool IsPE32(string path)
return reader.PEHeaders.PEHeader.Magic == PEMagic.PE32;
}
}

// Reads the Mach-O load commands and returns true if an LC_CODE_SIGNATURE command is found, otherwise returns false
static bool HasMachOSignatureLoadCommand(FileInfo file)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter for this change, but we may want this in a shared test helper so that we can add cross-platform checks for single-file publish with signing after dotnet/runtime#110417 goes in.

{
/* Mach-O files have the following structure:
* 32 byte header beginning with a magic number and info about the file and load commands
* A series of load commands with the following structure:
* - 4-byte command type
* - 4-byte command size
* - variable length command-specific data
*/
const uint LC_CODE_SIGNATURE = 0x1D;
using (var stream = file.OpenRead())
{
// Read the MachO magic number to determine endianness
Span<byte> eightByteBuffer = stackalloc byte[8];
stream.ReadExactly(eightByteBuffer);
// Determine if the magic number is in the same or opposite endianness as the runtime
bool reverseEndinanness = BitConverter.ToUInt32(eightByteBuffer.Slice(0, 4)) switch
{
0xFEEDFACF => false,
0xCFFAEDFE => true,
_ => throw new InvalidOperationException("Not a 64-bit Mach-O file")
};
// 4-byte value at offset 16 is the number of load commands
// 4-byte value at offset 20 is the size of the load commands
stream.Position = 16;
ReadUInts(stream, eightByteBuffer, out uint loadCommandsCount, out uint loadCommandsSize);
// Mach-0 64 byte headers are 32 bytes long, and the first load command will be right after
stream.Position = 32;
bool hasSignature = false;
for (int commandIndex = 0; commandIndex < loadCommandsCount; commandIndex++)
{
ReadUInts(stream, eightByteBuffer, out uint commandType, out uint commandSize);
if (commandType == LC_CODE_SIGNATURE)
{
hasSignature = true;
}
stream.Position += commandSize - eightByteBuffer.Length;
}
Debug.Assert(stream.Position == loadCommandsSize + 32);
return hasSignature;

void ReadUInts(Stream stream, Span<byte> buffer, out uint val1, out uint val2)
{
stream.ReadExactly(buffer);
val1 = BitConverter.ToUInt32(buffer.Slice(0, 4));
val2 = BitConverter.ToUInt32(buffer.Slice(4, 4));
if (reverseEndinanness)
{
val1 = BinaryPrimitives.ReverseEndianness(val1);
val2 = BinaryPrimitives.ReverseEndianness(val2);
}
}
}
}

}
}
Loading