From: Mateo Torres-Ruiz Date: Thu, 17 Jun 2021 22:09:27 +0000 (-0700) Subject: Codesign apphosts on Mac (#53913) X-Git-Tag: submit/tizen/20210909.063632~707 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=64942392cb3f588f0d68a792010e9716283c5b5d;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Codesign apphosts on Mac (#53913) * Add CodeSign to HostWriter * Fix test * PR feedback * Add EnableMacOSCodeSign to CreateAppHost Add tests * Check that OSPlatform is OSX before running codesign. * Guard from filepaths containing spaces * Move apphost exceptions to a single file Modify apphost exceptions inheritance * Move AppHostUpdateException * Apply suggestions from code review Co-authored-by: Jan Kotas * Add exit code to AppHostSigningException Co-authored-by: Jan Kotas --- diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostCustomizationUnsupportedOSException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostCustomizationUnsupportedOSException.cs deleted file mode 100644 index 0b0b055..0000000 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostCustomizationUnsupportedOSException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// The application host executable cannot be customized because adding resources requires - /// that the build be performed on Windows (excluding Nano Server). - /// - public class AppHostCustomizationUnsupportedOSException : AppHostUpdateException - { - } -} diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostExceptions.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostExceptions.cs new file mode 100644 index 0000000..ceae7c4 --- /dev/null +++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostExceptions.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.NET.HostModel.AppHost +{ + /// + /// An instance of this exception is thrown when an AppHost binary update + /// fails due to known user errors. + /// + public class AppHostUpdateException : Exception + { + internal AppHostUpdateException(string message = null) + : base(message) + { + } + } + + /// + /// The application host executable cannot be customized because adding resources requires + /// that the build be performed on Windows (excluding Nano Server). + /// + public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException + { + internal AppHostCustomizationUnsupportedOSException() + { + } + } + + /// + /// The MachO application host executable cannot be customized because + /// it was not in the expected format + /// + public sealed class AppHostMachOFormatException : AppHostUpdateException + { + public readonly MachOFormatError Error; + + internal AppHostMachOFormatException(MachOFormatError error) + { + Error = error; + } + } + + /// + /// Unable to use the input file as application host executable because it's not a + /// Windows executable for the CUI (Console) subsystem. + /// + public sealed class AppHostNotCUIException : AppHostUpdateException + { + internal AppHostNotCUIException() + { + } + } + + /// + /// Unable to use the input file as an application host executable + /// because it's not a Windows PE file + /// + public sealed class AppHostNotPEFileException : AppHostUpdateException + { + internal AppHostNotPEFileException() + { + } + } + + /// + /// Unable to sign the apphost binary. + /// + public sealed class AppHostSigningException : AppHostUpdateException + { + public readonly int ExitCode; + + internal AppHostSigningException(int exitCode, string signingErrorMessage) + : base(signingErrorMessage) + { + } + } + + /// + /// Given app file name is longer than 1024 bytes + /// + public sealed class AppNameTooLongException : AppHostUpdateException + { + public string LongName { get; } + + internal AppNameTooLongException(string name) + { + LongName = name; + } + } +} diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotCUIException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotCUIException.cs deleted file mode 100644 index b89a22d..0000000 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotCUIException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// Unable to use the input file as application host executable because it's not a - /// Windows executable for the CUI (Console) subsystem. - /// - public class AppHostNotCUIException : AppHostUpdateException - { - } -} diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotPEFileException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotPEFileException.cs deleted file mode 100644 index 6712d87..0000000 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotPEFileException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// Unable to use the input file as an application host executable - /// because it's not a Windows PE file - /// - public class AppHostNotPEFileException : AppHostUpdateException - { - } -} diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostUpdateException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostUpdateException.cs deleted file mode 100644 index 4570c77..0000000 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostUpdateException.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// An instance of this exception is thrown when an AppHost binary update - /// fails due to known user errors. - /// - public class AppHostUpdateException : Exception - { - } -} diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppNameTooLongException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppNameTooLongException.cs deleted file mode 100644 index 18a984b..0000000 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppNameTooLongException.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Microsoft.NET.HostModel.AppHost -{ - /// - /// Given app file name is longer than 1024 bytes - /// - public class AppNameTooLongException : AppHostUpdateException - { - public string LongName { get; } - public AppNameTooLongException(string name) - { - LongName = name; - } - - } -} diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs index 69ad0d4..c0e979d 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; @@ -30,12 +31,14 @@ namespace Microsoft.NET.HostModel.AppHost /// Full path to app binary or relative path to the result apphost file /// Specify whether to set the subsystem to GUI. Only valid for PE apphosts. /// Path to the intermediate assembly, used for copying resources to PE apphosts. + /// Sign the app binary using codesign with an anonymous certificate. public static void CreateAppHost( string appHostSourceFilePath, string appHostDestinationFilePath, string appBinaryFilePath, bool windowsGraphicalUserInterface = false, - string assemblyToCopyResorcesFrom = null) + string assemblyToCopyResorcesFrom = null, + bool enableMacOSCodeSign = false) { var bytesToWrite = Encoding.UTF8.GetBytes(appBinaryFilePath); if (bytesToWrite.Length > 1024) @@ -140,6 +143,9 @@ namespace Microsoft.NET.HostModel.AppHost { throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {filePermissionOctal} for {appHostDestinationFilePath}."); } + + if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + CodeSign(appHostDestinationFilePath); } } catch (Exception ex) @@ -233,6 +239,28 @@ namespace Microsoft.NET.HostModel.AppHost return headerOffset != 0; } + private static void CodeSign(string appHostPath) + { + Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); + const string codesign = @"/usr/bin/codesign"; + if (!File.Exists(codesign)) + return; + + var psi = new ProcessStartInfo() + { + Arguments = $"-s - \"{appHostPath}\"", + FileName = codesign, + RedirectStandardError = true, + }; + + using (var p = Process.Start(psi)) + { + p.WaitForExit(); + if (p.ExitCode != 0) + throw new AppHostSigningException(p.ExitCode, p.StandardError.ReadToEnd()); + } + } + [DllImport("libc", SetLastError = true)] private static extern int chmod(string pathname, int mode); } diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOFormatError.cs similarity index 75% rename from src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs rename to src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOFormatError.cs index 60e6d74e..eae935f 100644 --- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs +++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOFormatError.cs @@ -1,8 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; - namespace Microsoft.NET.HostModel.AppHost { /// @@ -25,18 +23,4 @@ namespace Microsoft.NET.HostModel.AppHost InvalidUTF8, // UTF8 decoding failed SignNotRemoved, // Signature not removed from the host (while processing a single-file bundle) } - - /// - /// The MachO application host executable cannot be customized because - /// it was not in the expected format - /// - public class AppHostMachOFormatException : AppHostUpdateException - { - public readonly MachOFormatError Error; - - public AppHostMachOFormatException(MachOFormatError error) - { - Error = error; - } - } } diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUpdateTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUpdateTests.cs index fc0aedb..3aa8886 100644 --- a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUpdateTests.cs +++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUpdateTests.cs @@ -12,6 +12,7 @@ using FluentAssertions; using Xunit; using Microsoft.NET.HostModel.AppHost; using Microsoft.DotNet.CoreSetup.Test; +using System.Diagnostics; namespace Microsoft.NET.HostModel.Tests { @@ -220,6 +221,109 @@ namespace Microsoft.NET.HostModel.Tests } } + [Theory] + [PlatformSpecific(TestPlatforms.OSX)] + [InlineData("")] + [InlineData("dir with spaces")] + public void CanCodeSignAppHostOnMacOS(string subdir) + { + using (TestDirectory testDirectory = TestDirectory.Create(subdir)) + { + string sourceAppHostMock = PrepareAppHostMockFile(testDirectory); + File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly); + string destinationFilePath = Path.Combine(testDirectory.Path, "DestinationAppHost.exe.mock"); + string appBinaryFilePath = "Test/App/Binary/Path.dll"; + HostWriter.CreateAppHost( + sourceAppHostMock, + destinationFilePath, + appBinaryFilePath, + windowsGraphicalUserInterface: false, + enableMacOSCodeSign: true); + + const string codesign = @"/usr/bin/codesign"; + var psi = new ProcessStartInfo() + { + Arguments = $"-d \"{destinationFilePath}\"", + FileName = codesign, + RedirectStandardError = true, + }; + + using (var p = Process.Start(psi)) + { + p.Start(); + p.StandardError.ReadToEnd() + .Should().Contain($"Executable=/private{Path.GetFullPath(destinationFilePath)}"); + p.WaitForExit(); + // Successfully signed the apphost. + Assert.True(p.ExitCode == 0, $"Expected exit code was '0' but '{codesign}' returned '{p.ExitCode}' instead."); + } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public void ItDoesNotCodeSignAppHostByDefault() + { + using (TestDirectory testDirectory = TestDirectory.Create()) + { + string sourceAppHostMock = PrepareAppHostMockFile(testDirectory); + File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly); + string destinationFilePath = Path.Combine(testDirectory.Path, "DestinationAppHost.exe.mock"); + string appBinaryFilePath = "Test/App/Binary/Path.dll"; + HostWriter.CreateAppHost( + sourceAppHostMock, + destinationFilePath, + appBinaryFilePath, + windowsGraphicalUserInterface: false); + + const string codesign = @"/usr/bin/codesign"; + var psi = new ProcessStartInfo() + { + Arguments = $"-d {destinationFilePath}", + FileName = codesign, + RedirectStandardError = true, + }; + + using (var p = Process.Start(psi)) + { + p.Start(); + p.StandardError.ReadToEnd() + .Should().Contain($"{Path.GetFullPath(destinationFilePath)}: code object is not signed at all"); + p.WaitForExit(); + } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public void CodeSigningFailuresThrow() + { + using (TestDirectory testDirectory = TestDirectory.Create()) + { + string sourceAppHostMock = PrepareAppHostMockFile(testDirectory); + File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly); + string destinationFilePath = Path.Combine(testDirectory.Path, "DestinationAppHost.exe.mock"); + string appBinaryFilePath = "Test/App/Binary/Path.dll"; + HostWriter.CreateAppHost( + sourceAppHostMock, + destinationFilePath, + appBinaryFilePath, + windowsGraphicalUserInterface: false, + enableMacOSCodeSign: true); + + // Run CreateAppHost again to sign the apphost a second time, + // causing codesign to fail. + var exception = Assert.Throws(() => + HostWriter.CreateAppHost( + sourceAppHostMock, + destinationFilePath, + appBinaryFilePath, + windowsGraphicalUserInterface: false, + enableMacOSCodeSign: true)); + Assert.Contains($"{destinationFilePath}: is already signed", exception.Message); + } + } + private string PrepareAppHostMockFile(TestDirectory testDirectory, Action customize = null) { // For now we're testing the AppHost on Windows PE files only. @@ -334,11 +438,12 @@ namespace Microsoft.NET.HostModel.Tests Directory.CreateDirectory(path); } - public static TestDirectory Create([CallerMemberName] string callingMethod = "") + public static TestDirectory Create([CallerMemberName] string callingMethod = "", string subDir = "") { string path = System.IO.Path.Combine( System.IO.Path.GetTempPath(), - "dotNetSdkUnitTest_" + callingMethod + (Guid.NewGuid().ToString().Substring(0, 8))); + "dotNetSdkUnitTest_" + callingMethod + (Guid.NewGuid().ToString().Substring(0, 8)), + subDir); return new TestDirectory(path); }