Codesign apphosts on Mac (#53913)
authorMateo Torres-Ruiz <mateoatr@users.noreply.github.com>
Thu, 17 Jun 2021 22:09:27 +0000 (15:09 -0700)
committerGitHub <noreply@github.com>
Thu, 17 Jun 2021 22:09:27 +0000 (15:09 -0700)
* 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 <jkotas@microsoft.com>
* Add exit code to AppHostSigningException

Co-authored-by: Jan Kotas <jkotas@microsoft.com>
src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostCustomizationUnsupportedOSException.cs [deleted file]
src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostExceptions.cs [new file with mode: 0644]
src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotCUIException.cs [deleted file]
src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostNotPEFileException.cs [deleted file]
src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostUpdateException.cs [deleted file]
src/installer/managed/Microsoft.NET.HostModel/AppHost/AppNameTooLongException.cs [deleted file]
src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOFormatError.cs [moved from src/installer/managed/Microsoft.NET.HostModel/AppHost/AppHostMachOFormatException.cs with 75% similarity]
src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.AppHost.Tests/AppHostUpdateTests.cs

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 (file)
index 0b0b055..0000000
+++ /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
-{
-    /// <summary>
-    /// The application host executable cannot be customized because adding resources requires
-    /// that the build be performed on Windows (excluding Nano Server).
-    /// </summary>
-    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 (file)
index 0000000..ceae7c4
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// An instance of this exception is thrown when an AppHost binary update
+    /// fails due to known user errors.
+    /// </summary>
+    public class AppHostUpdateException : Exception
+    {
+        internal AppHostUpdateException(string message = null)
+            : base(message)
+        {
+        }
+    }
+
+    /// <summary>
+    /// The application host executable cannot be customized because adding resources requires
+    /// that the build be performed on Windows (excluding Nano Server).
+    /// </summary>
+    public sealed class AppHostCustomizationUnsupportedOSException : AppHostUpdateException
+    {
+        internal AppHostCustomizationUnsupportedOSException()
+        {
+        }
+    }
+
+    /// <summary>
+    /// The MachO application host executable cannot be customized because
+    /// it was not in the expected format
+    /// </summary>
+    public sealed class AppHostMachOFormatException : AppHostUpdateException
+    {
+        public readonly MachOFormatError Error;
+
+        internal AppHostMachOFormatException(MachOFormatError error)
+        {
+            Error = error;
+        }
+    }
+
+    /// <summary>
+    /// Unable to use the input file as application host executable because it's not a
+    /// Windows executable for the CUI (Console) subsystem.
+    /// </summary>
+    public sealed class AppHostNotCUIException : AppHostUpdateException
+    {
+        internal AppHostNotCUIException()
+        {
+        }
+    }
+
+    /// <summary>
+    ///  Unable to use the input file as an application host executable
+    ///  because it's not a Windows PE file
+    /// </summary>
+    public sealed class AppHostNotPEFileException : AppHostUpdateException
+    {
+        internal AppHostNotPEFileException()
+        {
+        }
+    }
+
+    /// <summary>
+    /// Unable to sign the apphost binary.
+    /// </summary>
+    public sealed class AppHostSigningException : AppHostUpdateException
+    {
+        public readonly int ExitCode;
+
+        internal AppHostSigningException(int exitCode, string signingErrorMessage)
+            : base(signingErrorMessage)
+        {
+        }
+    }
+
+    /// <summary>
+    /// Given app file name is longer than 1024 bytes
+    /// </summary>
+    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 (file)
index b89a22d..0000000
+++ /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
-{
-    /// <summary>
-    /// Unable to use the input file as application host executable because it's not a
-    /// Windows executable for the CUI (Console) subsystem.
-    /// </summary>
-    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 (file)
index 6712d87..0000000
+++ /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
-{
-    /// <summary>
-    ///  Unable to use the input file as an application host executable
-    ///  because it's not a Windows PE file
-    /// </summary>
-    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 (file)
index 4570c77..0000000
+++ /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
-{
-    /// <summary>
-    /// An instance of this exception is thrown when an AppHost binary update
-    /// fails due to known user errors.
-    /// </summary>
-    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 (file)
index 18a984b..0000000
+++ /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
-{
-    /// <summary>
-    /// Given app file name is longer than 1024 bytes
-    /// </summary>
-    public class AppNameTooLongException : AppHostUpdateException
-    {
-        public string LongName { get; }
-        public AppNameTooLongException(string name)
-        {
-            LongName = name;
-        }
-
-    }
-}
index 69ad0d4..c0e979d 100644 (file)
@@ -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
         /// <param name="appBinaryFilePath">Full path to app binary or relative path to the result apphost file</param>
         /// <param name="windowsGraphicalUserInterface">Specify whether to set the subsystem to GUI. Only valid for PE apphosts.</param>
         /// <param name="assemblyToCopyResorcesFrom">Path to the intermediate assembly, used for copying resources to PE apphosts.</param>
+        /// <param name="enableMacOSCodeSign">Sign the app binary using codesign with an anonymous certificate.</param>
         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);
     }
@@ -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
 {
     /// <summary>
@@ -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)
     }
-
-    /// <summary>
-    /// The MachO application host executable cannot be customized because
-    /// it was not in the expected format
-    /// </summary>
-    public class AppHostMachOFormatException : AppHostUpdateException
-    {
-        public readonly MachOFormatError Error;
-
-        public AppHostMachOFormatException(MachOFormatError error)
-        {
-            Error = error;
-        }
-    }
 }
index fc0aedb..3aa8886 100644 (file)
@@ -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<AppHostSigningException>(() =>
+                    HostWriter.CreateAppHost(
+                    sourceAppHostMock,
+                    destinationFilePath,
+                    appBinaryFilePath,
+                    windowsGraphicalUserInterface: false,
+                    enableMacOSCodeSign: true));
+                Assert.Contains($"{destinationFilePath}: is already signed", exception.Message);
+            }
+        }
+
         private string PrepareAppHostMockFile(TestDirectory testDirectory, Action<byte[]> 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);
             }