diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets index af9a88f9cd89..269f1a45c5d7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets @@ -803,7 +803,7 @@ Copyright (c) .NET Foundation. All rights reserved. Condition="'$(UseAppHost)' == 'true' and '$(_IsExecutable)' == 'true'"> <_UseWindowsGraphicalUserInterface Condition="($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win'))) and '$(OutputType)'=='WinExe'">true - <_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 <_UseSingleFileHostForPublish Condition="'$(PublishSingleFile)' == 'true' and '$(SelfContained)' == 'true' and diff --git a/test/Microsoft.NET.Build.Tests/AppHostTests.cs b/test/Microsoft.NET.Build.Tests/AppHostTests.cs index 56fe74b79d59..6cc6b213f5b3 100644 --- a/test/Microsoft.NET.Build.Tests/AppHostTests.cs +++ b/test/Microsoft.NET.Build.Tests/AppHostTests.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Buffers.Binary; using System.Diagnostics; using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; @@ -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() { $"/p:RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}" }; - if (!selfContained) - buildArgs.Add("/p:PublishSingleFile=true"); - + var buildArgs = new List() { $"/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] @@ -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) + { + /* 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 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 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); + } + } + } + } + } }