From 5d69e2dca30524a93b00cd613be218144b5f95d1 Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Tue, 12 Nov 2024 14:15:41 -0600
Subject: [PATCH] Add managed MachO signing (#108992)
Creates a managed implementation of Mach-O object file signing for the apphost.
This signer is byte-for-byte identical to the output of codesign except for padding at the end of the file and the corresponding size fields in the headers.
The MachObjectFile and CodeSignature store all the relevant headers and signature information to add/remove a signature from a Mach Object. Most other files are details regarding header format and reading files with different endianness.
The new signer uses a MemoryMappedViewAccessor instead of a Stream. Since memory mapped files can't be resized, MachObjectFile.CreateAdHocSignature() will write out the headers and load commands to the memory mapped file, but not the signature blob. The signature blob will instead be written to the destination file stream after the rest of the file is copied from the memory mapped file.
The signature is composed of an EmbeddedSignature superblob with an ID and offset of 3 following blobs, followed by a CodeDirectory blob, an empty Requirements blob, and an empty CMS blob. The empty Requirements blob and an empty CMS blob are not strictly required but are added for compatibility with codesign. The CodeDirectory includes the identifier string, hashes of each page of the file and information required to verify the hashes.
The signing process for an unsigned executable extends the __LINKEDIT segment (which is always at the end of the file) to make room for the signature blob, writes the signature blob to that location, and adds a CodeSignature load command that points the the signature. If the executable was already signed, it simply clears the old signature and replaces it, extends or shrinks the existing __LINKEDIT size and updates the existing CodeSignature load command.
Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com>
---
eng/Version.Details.xml | 4 +
eng/Versions.props | 1 +
.../AppHost/HostWriter.cs | 85 +++--
.../AppHost/MachOUtils.cs | 308 +---------------
.../AssemblyAttributes.cs | 16 +
.../Microsoft.NET.HostModel/Bundle/Bundler.cs | 27 +-
.../Bundle/Codesign.cs | 38 ++
.../Bundle/TargetInfo.cs | 3 +-
.../MachO/BinaryFormat/BlobIndex.cs | 26 ++
.../MachO/BinaryFormat/CmsBlob.cs | 28 ++
.../MachO/BinaryFormat/CodeDirectoryHeader.cs | 103 ++++++
.../BinaryFormat/EmbeddedSignatureHeader.cs | 30 ++
.../MachO/BinaryFormat/LinkEditCommand.cs | 35 ++
.../MachO/BinaryFormat/LoadCommand.cs | 20 ++
.../MachO/BinaryFormat/MachHeader.cs | 31 ++
.../MachO/BinaryFormat/NameBuffer.cs | 37 ++
.../MachO/BinaryFormat/RequirementsBlob.cs | 50 +++
.../BinaryFormat/Section64LoadCommand.cs | 29 ++
.../BinaryFormat/Segment64LoadCommand.cs | 34 ++
.../MachO/EndianConversionExtensions.cs | 29 ++
.../MachO/Enums/BlobMagic.cs | 15 +
.../MachO/Enums/CodeDirectoryFlags.cs | 12 +
.../MachO/Enums/CodeDirectorySpecialSlot.cs | 14 +
.../MachO/Enums/CodeDirectoryVersion.cs | 18 +
.../MachO/Enums/ExecutableSegmentFlags.cs | 12 +
.../MachO/Enums/HashType.cs | 12 +
.../MachO/Enums/MachFileType.cs | 9 +
.../MachO/Enums/MachLoadCommandType.cs | 15 +
.../MachO/Enums/MachMagic.cs | 17 +
.../MachO/MachMagicExtensions.cs | 34 ++
.../MachO/MachObjectFile.CodeSignature.cs | 274 ++++++++++++++
.../MachO/MachObjectFile.cs | 334 ++++++++++++++++++
.../Microsoft.NET.HostModel.csproj | 3 +-
src/installer/pkg/THIRD-PARTY-NOTICES.TXT | 4 +-
.../MachOHostSigningTests.cs | 35 ++
.../SelfContainedAppLaunch.cs | 5 +-
.../AppHost/CreateAppHost.cs | 109 ++++--
.../MachObjectSigning/SigningTests.cs | 241 +++++++++++++
.../MachObjectSigning/TestData.cs | 39 ++
.../Microsoft.NET.HostModel.Tests.csproj | 4 +
.../tests/TestUtils/SingleFileTestApp.cs | 3 +-
41 files changed, 1753 insertions(+), 390 deletions(-)
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/AssemblyAttributes.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/Bundle/Codesign.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/BlobIndex.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CmsBlob.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CodeDirectoryHeader.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/EmbeddedSignatureHeader.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LinkEditCommand.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LoadCommand.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/MachHeader.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/NameBuffer.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/RequirementsBlob.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Section64LoadCommand.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Segment64LoadCommand.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/EndianConversionExtensions.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryFlags.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryVersion.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/ExecutableSegmentFlags.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/HashType.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachFileType.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachLoadCommandType.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachMagic.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/MachMagicExtensions.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.CodeSignature.cs
create mode 100644 src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
create mode 100644 src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
create mode 100644 src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
create mode 100644 src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/TestData.cs
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index 4e490f57f803a..25bc4a9bf536b 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -474,5 +474,9 @@
https://github.com/dotnet/node
78c56619da525bd37de4c2828092762fb4fa03c4
+
+ https://github.com/dotnet/runtime-assets
+ 24f902e6d5bfe3fec9f07d55efe44794aec614a1
+
diff --git a/eng/Versions.props b/eng/Versions.props
index 9c0d9954d8294..e45388f0a957d 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -154,6 +154,7 @@
10.0.0-beta.24530.1
10.0.0-beta.24530.1
10.0.0-beta.24530.1
+ 10.0.0-beta.24522.1
1.0.0-prerelease.24462.2
1.0.0-prerelease.24462.2
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
index 9c08bd32fd3fd..29dda4912d2fc 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs
@@ -7,6 +7,7 @@
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Text;
+using Microsoft.NET.HostModel.MachO;
namespace Microsoft.NET.HostModel.AppHost
{
@@ -60,7 +61,7 @@ public enum SearchLocation : byte
/// 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.
+ /// Sign the app binary with an anonymous certificate. Only use when the AppHost is a Mach-O file built for MacOS.
/// Remove CET Shadow Stack compatibility flag if set
/// Options for how the created apphost should look for the .NET install
public static void CreateAppHost(
@@ -118,48 +119,55 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
{
RetryUtil.RetryOnIOError(() =>
{
- FileStream appHostSourceStream = null;
- MemoryMappedFile memoryMappedFile = null;
- MemoryMappedViewAccessor memoryMappedViewAccessor = null;
- try
+ bool isMachOImage;
+ using (FileStream appHostDestinationStream = new FileStream(appHostDestinationFilePath, FileMode.Create, FileAccess.ReadWrite))
{
- // Open the source host file.
- appHostSourceStream = new FileStream(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1);
- memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
- memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
-
+ using (FileStream appHostSourceStream = new(appHostSourceFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
+ {
+ isMachOImage = MachObjectFile.IsMachOImage(appHostSourceStream);
+ if (!isMachOImage && enableMacOSCodeSign)
+ {
+ throw new InvalidDataException("Cannot sign a non-Mach-O file.");
+ }
+ appHostSourceStream.CopyTo(appHostDestinationStream);
+ }
// Get the size of the source app host to ensure that we don't write extra data to the destination.
// On Windows, the size of the view accessor is rounded up to the next page boundary.
- long sourceAppHostLength = appHostSourceStream.Length;
-
- // Transform the host file in-memory.
- RewriteAppHost(memoryMappedFile, memoryMappedViewAccessor);
-
- // Save the transformed host.
- using (FileStream fileStream = new FileStream(appHostDestinationFilePath, FileMode.Create))
+ long appHostLength = appHostDestinationStream.Length;
+ string destinationFileName = Path.GetFileName(appHostDestinationFilePath);
+ // On Mac, we need to extend the file size to accommodate the signature.
+ long appHostTmpCapacity = enableMacOSCodeSign ?
+ appHostLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostLength, destinationFileName)
+ : appHostLength;
+
+ using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationStream, null, appHostTmpCapacity, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
+ using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, appHostTmpCapacity, MemoryMappedFileAccess.ReadWrite))
{
- BinaryUtils.WriteToStream(memoryMappedViewAccessor, fileStream, sourceAppHostLength);
-
- // Remove the signature from MachO hosts.
- if (!appHostIsPEImage)
+ // Transform the host file in-memory.
+ RewriteAppHost(memoryMappedFile, memoryMappedViewAccessor);
+ if (isMachOImage)
{
- MachOUtils.RemoveSignature(fileStream);
+ if (enableMacOSCodeSign)
+ {
+ string fileName = Path.GetFileName(appHostDestinationFilePath);
+ MachObjectFile machObjectFile = MachObjectFile.Create(memoryMappedViewAccessor);
+ appHostLength = machObjectFile.CreateAdHocSignature(memoryMappedViewAccessor, fileName);
+ }
+ else if (MachObjectFile.TryRemoveCodesign(memoryMappedViewAccessor, out long? length))
+ {
+ appHostLength = length.Value;
+ }
}
+ }
+ appHostDestinationStream.SetLength(appHostLength);
- if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
- {
- using var updater = new ResourceUpdater(fileStream, true);
- updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
- updater.Update();
- }
+ if (assemblyToCopyResourcesFrom != null && appHostIsPEImage)
+ {
+ using var updater = new ResourceUpdater(appHostDestinationStream, true);
+ updater.AddResourcesFromPEImage(assemblyToCopyResourcesFrom);
+ updater.Update();
}
}
- finally
- {
- memoryMappedViewAccessor?.Dispose();
- memoryMappedFile?.Dispose();
- appHostSourceStream?.Dispose();
- }
});
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -178,15 +186,6 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not set file permission {Convert.ToString(filePermissionOctal, 8)} for {appHostDestinationFilePath}.");
}
-
- if (enableMacOSCodeSign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable())
- {
- (int exitCode, string stdErr) = HostModelUtils.RunCodesign("-s -", appHostDestinationFilePath);
- if (exitCode != 0)
- {
- throw new AppHostSigningException(exitCode, stdErr);
- }
- }
}
}
catch (Exception ex)
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs b/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
index b0da962c1ab10..33e62df2b9221 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/AppHost/MachOUtils.cs
@@ -5,7 +5,9 @@
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text;
+using Microsoft.NET.HostModel.MachO;
namespace Microsoft.NET.HostModel.AppHost
{
@@ -17,75 +19,7 @@ internal static class MachOUtils
// The data fields and enumerations match the structure definitions in the above file,
// and hence do not conform to C# CoreFx naming style.
- private enum Magic : uint
- {
- MH_MAGIC = 0xfeedface,
- MH_CIGAM = 0xcefaedfe,
- MH_MAGIC_64 = 0xfeedfacf,
- MH_CIGAM_64 = 0xcffaedfe
- }
-
- private enum FileType : uint
- {
- MH_EXECUTE = 0x2
- }
-
-#pragma warning disable 0649
- private struct MachHeader
- {
- public Magic magic;
- public int cputype;
- public int cpusubtype;
- public FileType filetype;
- public uint ncmds;
- public uint sizeofcmds;
- public uint flags;
- public uint reserved;
-
- public bool Is64BitExecutable()
- {
- return magic == Magic.MH_MAGIC_64 && filetype == FileType.MH_EXECUTE;
- }
-
- public bool IsValid()
- {
- switch (magic)
- {
- case Magic.MH_CIGAM:
- case Magic.MH_CIGAM_64:
- case Magic.MH_MAGIC:
- case Magic.MH_MAGIC_64:
- return true;
-
- default:
- return false;
- }
- }
- }
-
- private enum Command : uint
- {
- LC_SYMTAB = 0x2,
- LC_SEGMENT_64 = 0x19,
- LC_CODE_SIGNATURE = 0x1d,
- }
-
- private struct LoadCommand
- {
- public Command cmd;
- public uint cmdsize;
- }
-
- // The linkedit_data_command contains the offsets and sizes of a blob
- // of data in the __LINKEDIT segment (including LC_CODE_SIGNATURE).
- private struct LinkEditDataCommand
- {
- public Command cmd;
- public uint cmdsize;
- public uint dataoff;
- public uint datasize;
- }
-
+ [StructLayout(LayoutKind.Sequential)]
private struct SymtabCommand
{
public uint cmd;
@@ -96,44 +30,6 @@ private struct SymtabCommand
public uint strsize;
};
- private unsafe struct SegmentCommand64
- {
- public Command cmd;
- public uint cmdsize;
- public fixed byte segname[16];
- public ulong vmaddr;
- public ulong vmsize;
- public ulong fileoff;
- public ulong filesize;
- public int maxprot;
- public int initprot;
- public uint nsects;
- public uint flags;
-
- public string SegName
- {
- get
- {
- fixed (byte* p = segname)
- {
- int len = 0;
- while (*(p + len) != 0 && len++ < 16) ;
-
- try
- {
- return Encoding.UTF8.GetString(p, len);
- }
- catch (ArgumentException)
- {
- throw new AppHostMachOFormatException(MachOFormatError.InvalidUTF8);
- }
- }
- }
- }
- }
-
-#pragma warning restore 0649
-
private static void Verify(bool condition, MachOFormatError error)
{
if (!condition)
@@ -142,180 +38,6 @@ private static void Verify(bool condition, MachOFormatError error)
}
}
- public static bool IsMachOImage(string filePath)
- {
- using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
- {
- if (reader.BaseStream.Length < 256) // Header size
- {
- return false;
- }
-
- uint magic = reader.ReadUInt32();
- return Enum.IsDefined(typeof(Magic), magic);
- }
- }
-
- ///
- /// This Method is a utility to remove the code-signature (if any)
- /// from a MachO AppHost binary.
- ///
- /// The tool assumes the following layout of the executable:
- ///
- /// * MachoHeader (64-bit, executable, not swapped integers)
- /// * LoadCommands
- /// LC_SEGMENT_64 (__PAGEZERO)
- /// LC_SEGMENT_64 (__TEXT)
- /// LC_SEGMENT_64 (__DATA)
- /// LC_SEGMENT_64 (__LINKEDIT)
- /// ...
- /// LC_SYMTAB
- /// ...
- /// LC_CODE_SIGNATURE (last)
- ///
- /// * ... Different Segments ...
- ///
- /// * The __LINKEDIT Segment (last)
- /// * ... Different sections ...
- /// * SYMTAB
- /// * (Some alignment bytes)
- /// * The Code-signature
- ///
- /// In order to remove the signature, the method:
- /// - Removes (zeros out) the LC_CODE_SIGNATURE command
- /// - Adjusts the size and count of the load commands in the header
- /// - Truncates the size of the __LINKEDIT segment to the end of SYMTAB
- /// - Truncates the apphost file to the end of the __LINKEDIT segment
- ///
- ///
- /// Stream containing the AppHost
- ///
- /// True if
- /// - The input is a MachO binary, and
- /// - It is a signed binary, and
- /// - The signature was successfully removed
- /// False otherwise
- ///
- ///
- /// The input is a MachO file, but doesn't match the expect format of the AppHost.
- ///
- public static unsafe bool RemoveSignature(FileStream stream)
- {
- uint signatureSize = 0;
- using (var mappedFile = MemoryMappedFile.CreateFromFile(stream,
- mapName: null,
- capacity: 0,
- MemoryMappedFileAccess.ReadWrite,
- HandleInheritability.None,
- leaveOpen: true))
- {
- using (var accessor = mappedFile.CreateViewAccessor())
- {
- byte* file = null;
- try
- {
- accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref file);
- Verify(file != null, MachOFormatError.MemoryMapAccessFault);
-
- MachHeader* header = (MachHeader*)file;
-
- if (!header->IsValid())
- {
- // Not a MachO file.
- return false;
- }
-
- Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
-
- file += sizeof(MachHeader);
- SegmentCommand64* linkEdit = null;
- SymtabCommand* symtab = null;
- LinkEditDataCommand* signature = null;
-
- for (uint i = 0; i < header->ncmds; i++)
- {
- LoadCommand* command = (LoadCommand*)file;
- if (command->cmd == Command.LC_SEGMENT_64)
- {
- SegmentCommand64* segment = (SegmentCommand64*)file;
- if (segment->SegName.Equals("__LINKEDIT"))
- {
- Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
- linkEdit = segment;
- }
- }
- else if (command->cmd == Command.LC_SYMTAB)
- {
- Verify(symtab == null, MachOFormatError.DuplicateSymtab);
- symtab = (SymtabCommand*)command;
- }
- else if (command->cmd == Command.LC_CODE_SIGNATURE)
- {
- Verify(i == header->ncmds - 1, MachOFormatError.SignCommandNotLast);
- signature = (LinkEditDataCommand*)command;
- break;
- }
-
- file += command->cmdsize;
- }
-
- if (signature != null)
- {
- Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
- Verify(symtab != null, MachOFormatError.MissingSymtab);
-
- var symtabEnd = symtab->stroff + symtab->strsize;
- var linkEditEnd = linkEdit->fileoff + linkEdit->filesize;
- var signatureEnd = signature->dataoff + signature->datasize;
- var fileEnd = (ulong)stream.Length;
-
- Verify(linkEditEnd == fileEnd, MachOFormatError.LinkEditNotLast);
- Verify(signatureEnd == fileEnd, MachOFormatError.SignBlobNotLast);
-
- Verify(symtab->symoff > linkEdit->fileoff, MachOFormatError.SymtabNotInLinkEdit);
- Verify(signature->dataoff > linkEdit->fileoff, MachOFormatError.SignNotInLinkEdit);
-
- // The signature blob immediately follows the symtab blob,
- // except for a few bytes of padding.
- Verify(signature->dataoff >= symtabEnd && signature->dataoff - symtabEnd < 32, MachOFormatError.SignBlobNotLast);
-
- // Remove the signature command
- header->ncmds--;
- header->sizeofcmds -= signature->cmdsize;
- Unsafe.InitBlock(signature, 0, signature->cmdsize);
-
- // Remove the signature blob (note for truncation)
- signatureSize = (uint)(fileEnd - symtabEnd);
-
- // Adjust the __LINKEDIT segment load command
- linkEdit->filesize -= signatureSize;
-
- // codesign --remove-signature doesn't reset the vmsize.
- // Setting the vmsize here makes the output bin-equal with the original
- // unsigned apphost (and not bin-equal with a signed-unsigned-apphost).
- linkEdit->vmsize = linkEdit->filesize;
- }
- }
- finally
- {
- if (file != null)
- {
- accessor.SafeMemoryMappedViewHandle.ReleasePointer();
- }
- }
- }
- }
-
- if (signatureSize != 0)
- {
- // The signature was removed, update the file length
- stream.SetLength(stream.Length - signatureSize);
- return true;
- }
-
- return false;
- }
-
///
/// This Method is a utility to adjust the apphost MachO-header
/// to include the bytes added by the single-file bundler at the end of the file.
@@ -380,38 +102,38 @@ public static unsafe bool AdjustHeadersForBundle(string filePath)
MachHeader* header = (MachHeader*)file;
- if (!header->IsValid())
+ if (!MachObjectFile.IsMachOImage(accessor))
{
// Not a MachO file.
return false;
}
- Verify(header->Is64BitExecutable(), MachOFormatError.Not64BitExe);
+ Verify(header->Is64Bit, MachOFormatError.Not64BitExe);
file += sizeof(MachHeader);
- SegmentCommand64* linkEdit = null;
+ Segment64LoadCommand* linkEdit = null;
SymtabCommand* symtab = null;
- LinkEditDataCommand* signature = null;
+ LinkEditCommand* signature = null;
- for (uint i = 0; i < header->ncmds; i++)
+ for (uint i = 0; i < header->NumberOfCommands; i++)
{
LoadCommand* command = (LoadCommand*)file;
- if (command->cmd == Command.LC_SEGMENT_64)
+ if (command->GetCommandType(*header) == MachLoadCommandType.Segment64)
{
- SegmentCommand64* segment = (SegmentCommand64*)file;
- if (segment->SegName.Equals("__LINKEDIT"))
+ Segment64LoadCommand* segment = (Segment64LoadCommand*)file;
+ if (segment->Name.Equals(NameBuffer.__LINKEDIT))
{
Verify(linkEdit == null, MachOFormatError.DuplicateLinkEdit);
linkEdit = segment;
}
}
- else if (command->cmd == Command.LC_SYMTAB)
+ else if (command->GetCommandType(*header) == MachLoadCommandType.SymbolTable)
{
Verify(symtab == null, MachOFormatError.DuplicateSymtab);
symtab = (SymtabCommand*)command;
}
- file += command->cmdsize;
+ file += command->GetCommandSize(*header);
}
Verify(linkEdit != null, MachOFormatError.MissingLinkEdit);
@@ -427,8 +149,8 @@ public static unsafe bool AdjustHeadersForBundle(string filePath)
symtab->strsize = (uint)newStringTableSize;
// Update the __LINKEDIT segment to include bundle-data
- linkEdit->filesize = fileLength - linkEdit->fileoff;
- linkEdit->vmsize = linkEdit->filesize;
+ linkEdit->SetFileSize(fileLength - linkEdit->GetFileOffset(*header), *header);
+ linkEdit->SetVMSize(linkEdit->GetFileSize(*header), *header);
}
finally
{
diff --git a/src/installer/managed/Microsoft.NET.HostModel/AssemblyAttributes.cs b/src/installer/managed/Microsoft.NET.HostModel/AssemblyAttributes.cs
new file mode 100644
index 0000000000000..1fbc040b16ff0
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/AssemblyAttributes.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.NET.HostModel.Tests, PublicKey="
+ + "00240000048000009400000006020000"
+ + "00240000525341310004000001000100"
+ + "b5fc90e7027f67871e773a8fde8938c8"
+ + "1dd402ba65b9201d60593e96c492651e"
+ + "889cc13f1415ebb53fac1131ae0bd333"
+ + "c5ee6021672d9718ea31a8aebd0da007"
+ + "2f25d87dba6fc90ffd598ed4da35e44c"
+ + "398c454307e8e33b8426143daec9f596"
+ + "836f97c8f74750e5975c64e2189f45de"
+ + "f46b2a2b1247adc3652bf5c308055da9")]
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
index a4333dabc45a1..a548a5358e28b 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
@@ -9,6 +9,7 @@
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
+using Microsoft.DotNet.CoreSetup;
using Microsoft.NET.HostModel.AppHost;
namespace Microsoft.NET.HostModel.Bundle
@@ -272,9 +273,9 @@ public string GenerateBundle(IReadOnlyList fileSpecs)
BinaryUtils.CopyFile(hostSource, bundlePath);
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable())
+ if (_target.IsOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Codesign.IsAvailable)
{
- RemoveCodesignIfNecessary(bundlePath);
+ Codesign.Run("--remove-signature", bundlePath);
}
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
@@ -346,9 +347,10 @@ public string GenerateBundle(IReadOnlyList fileSpecs)
HostWriter.SetAsBundle(bundlePath, headerOffset);
// Sign the bundle if requested
- if (_macosCodesign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && HostModelUtils.IsCodesignAvailable())
+ // TODO: use managed code signing
+ if (_macosCodesign && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Codesign.IsAvailable)
{
- var (exitCode, stdErr) = HostModelUtils.RunCodesign("-s -", bundlePath);
+ var (exitCode, stdErr) = Codesign.Run("-s -", bundlePath);
if (exitCode != 0)
{
throw new InvalidOperationException($"Failed to codesign '{bundlePath}': {stdErr}");
@@ -356,23 +358,6 @@ public string GenerateBundle(IReadOnlyList fileSpecs)
}
return bundlePath;
-
- // Remove mac code signature if applied before bundling
- static void RemoveCodesignIfNecessary(string bundlePath)
- {
- Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
- Debug.Assert(HostModelUtils.IsCodesignAvailable());
-
- // `codesign -v` returns 0 if app is signed
- if (HostModelUtils.RunCodesign("-v", bundlePath).ExitCode == 0)
- {
- var (exitCode, stdErr) = HostModelUtils.RunCodesign("--remove-signature", bundlePath);
- if (exitCode != 0)
- {
- throw new InvalidOperationException($"Removing codesign from '{bundlePath}' failed: {stdErr}");
- }
- }
- }
}
}
}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/Codesign.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Codesign.cs
new file mode 100644
index 0000000000000..e31f8bc706ad7
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/Codesign.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.DotNet.CoreSetup
+{
+ public class Codesign
+ {
+ private const string CodesignPath = "/usr/bin/codesign";
+
+ public static bool IsAvailable => File.Exists(CodesignPath);
+
+ public static (int ExitCode, string StdErr) Run(string args, string binaryPath)
+ {
+ Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
+ Debug.Assert(IsAvailable);
+
+ ProcessStartInfo psi = new()
+ {
+ Arguments = $"{args} \"{binaryPath}\"",
+ FileName = CodesignPath,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ };
+
+ using (var p = Process.Start(psi))
+ {
+ if (p == null)
+ return (-1, "Failed to start process");
+ p.WaitForExit();
+ return (p.ExitCode, p.StandardError.ReadToEnd());
+ }
+ }
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Bundle/TargetInfo.cs b/src/installer/managed/Microsoft.NET.HostModel/Bundle/TargetInfo.cs
index 1876c7e379050..87c644272379e 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Bundle/TargetInfo.cs
+++ b/src/installer/managed/Microsoft.NET.HostModel/Bundle/TargetInfo.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.NET.HostModel.MachO;
using Microsoft.NET.HostModel.AppHost;
using System;
using System.Diagnostics;
@@ -77,7 +78,7 @@ public TargetInfo(OSPlatform? os, Architecture? arch, Version targetFrameworkVer
public bool IsNativeBinary(string filePath)
{
- return IsWindows ? PEUtils.IsPEImage(filePath) : IsOSX ? MachOUtils.IsMachOImage(filePath) : ElfUtils.IsElfImage(filePath);
+ return IsWindows ? PEUtils.IsPEImage(filePath) : IsOSX ? MachObjectFile.IsMachOImage(filePath) : ElfUtils.IsElfImage(filePath);
}
public string GetAssemblyName(string hostName)
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/BlobIndex.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/BlobIndex.cs
new file mode 100644
index 0000000000000..21c70e657f614
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/BlobIndex.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// Format based off of https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/cscdefs.h#L18
+/// Code signature data is always big endian / network order.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct BlobIndex
+{
+ private readonly CodeDirectorySpecialSlot _slot;
+ private readonly uint _offset;
+
+ public CodeDirectorySpecialSlot Slot => (CodeDirectorySpecialSlot)((uint)_slot).ConvertFromBigEndian();
+ public uint Offset => _offset.ConvertFromBigEndian();
+
+ public BlobIndex(CodeDirectorySpecialSlot slot, uint offset)
+ {
+ _slot = (CodeDirectorySpecialSlot)((uint)slot).ConvertToBigEndian();
+ _offset = offset.ConvertToBigEndian();
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CmsBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CmsBlob.cs
new file mode 100644
index 0000000000000..227d6183971a6
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CmsBlob.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_utilities/lib/blob.h
+/// Code signature data is always big endian / network order.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct CmsWrapperBlob
+{
+ private BlobMagic _magic;
+ private uint _length;
+
+ public static CmsWrapperBlob Empty = GetEmptyBlob();
+
+ private static unsafe CmsWrapperBlob GetEmptyBlob()
+ {
+ return new CmsWrapperBlob
+ {
+ _magic = (BlobMagic)((uint)BlobMagic.CmsWrapper).ConvertToBigEndian(),
+ _length = ((uint)sizeof(CmsWrapperBlob)).ConvertToBigEndian()
+ };
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CodeDirectoryHeader.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CodeDirectoryHeader.cs
new file mode 100644
index 0000000000000..926a60c8b421d
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/CodeDirectoryHeader.cs
@@ -0,0 +1,103 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// For code signature version 0x20400 only. Code signature headers/blobs are all big endian / network order.
+///
+///
+/// Format based off of https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/codedirectory.h#L193
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct CodeDirectoryHeader
+{
+ private readonly BlobMagic _magic = (BlobMagic)((uint)BlobMagic.CodeDirectory).ConvertToBigEndian();
+ private uint _size;
+ private CodeDirectoryVersion _version;
+ private CodeDirectoryFlags _flags;
+ private uint _hashesOffset;
+ private uint _identifierOffset;
+ private uint _specialSlotCount;
+ private uint _codeSlotCount;
+ private uint _executableLength;
+ public byte HashSize;
+ public HashType HashType;
+ public byte Platform;
+ public byte Log2PageSize;
+ private readonly uint _reserved = 0;
+ private readonly uint _scatterOffset = 0;
+ private readonly uint _teamIdOffset = 0;
+ private readonly uint _reserved2 = 0;
+ private ulong _codeLimit64;
+ private ulong _execSegmentBase;
+ private ulong _execSegmentLimit;
+ private ExecutableSegmentFlags _execSegmentFlags;
+
+ public CodeDirectoryHeader()
+ {
+ }
+
+ public uint Size
+ {
+ get => _size.ConvertFromBigEndian();
+ set => _size = value.ConvertToBigEndian();
+ }
+ public CodeDirectoryVersion Version
+ {
+ get => (CodeDirectoryVersion)((uint)_version).ConvertFromBigEndian();
+ set => _version = (CodeDirectoryVersion)((uint)value).ConvertToBigEndian();
+ }
+ public CodeDirectoryFlags Flags
+ {
+ get => (CodeDirectoryFlags)((uint)_flags).ConvertFromBigEndian();
+ set => _flags = (CodeDirectoryFlags)((uint)value).ConvertToBigEndian();
+ }
+ public uint HashesOffset
+ {
+ get => _hashesOffset.ConvertFromBigEndian();
+ set => _hashesOffset = value.ConvertToBigEndian();
+ }
+ public uint IdentifierOffset
+ {
+ get => _identifierOffset.ConvertFromBigEndian();
+ set => _identifierOffset = value.ConvertToBigEndian();
+ }
+ public uint SpecialSlotCount
+ {
+ get => _specialSlotCount.ConvertFromBigEndian();
+ set => _specialSlotCount = value.ConvertToBigEndian();
+ }
+ public uint CodeSlotCount
+ {
+ get => _codeSlotCount.ConvertFromBigEndian();
+ set => _codeSlotCount = value.ConvertToBigEndian();
+ }
+ public uint ExecutableLength
+ {
+ get => _executableLength.ConvertFromBigEndian();
+ set => _executableLength = value.ConvertToBigEndian();
+ }
+ public ulong CodeLimit64
+ {
+ get => _codeLimit64.ConvertFromBigEndian();
+ set => _codeLimit64 = value.ConvertToBigEndian();
+ }
+ public ulong ExecSegmentBase
+ {
+ get => _execSegmentBase.ConvertFromBigEndian();
+ set => _execSegmentBase = value.ConvertToBigEndian();
+ }
+ public ulong ExecSegmentLimit
+ {
+ get => _execSegmentLimit.ConvertFromBigEndian();
+ set => _execSegmentLimit = value.ConvertToBigEndian();
+ }
+ public ExecutableSegmentFlags ExecSegmentFlags
+ {
+ get => (ExecutableSegmentFlags)((ulong)_execSegmentFlags).ConvertFromBigEndian();
+ set => _execSegmentFlags = (ExecutableSegmentFlags)((ulong)value).ConvertToBigEndian();
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/EmbeddedSignatureHeader.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/EmbeddedSignatureHeader.cs
new file mode 100644
index 0000000000000..5d7791976cacb
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/EmbeddedSignatureHeader.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// Format based off of https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/cscdefs.h#L23
+/// Code Signature data is always big endian / network order.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct EmbeddedSignatureHeader
+{
+ private readonly BlobMagic _magic = (BlobMagic)((uint)BlobMagic.EmbeddedSignature).ConvertToBigEndian();
+ private uint _size;
+ private readonly uint _blobCount = 3u.ConvertToBigEndian();
+ public BlobIndex CodeDirectory;
+ public BlobIndex Requirements;
+ public BlobIndex CmsWrapper;
+
+ public EmbeddedSignatureHeader() { }
+
+ public uint BlobCount => _blobCount.ConvertFromBigEndian();
+ public uint Size
+ {
+ get => _size.ConvertFromBigEndian();
+ set => _size = value.ConvertToBigEndian();
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LinkEditCommand.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LinkEditCommand.cs
new file mode 100644
index 0000000000000..db0d06ca2b366
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LinkEditCommand.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// A load command that provides information about an item in the __LINKEDIT segment.
+/// We only care about this when the _command is CodeSignature.
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/include/mach-o/loader.h#L1232 for reference.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct LinkEditCommand
+{
+ private readonly MachLoadCommandType _command;
+ private readonly uint _commandSize;
+ private readonly uint _dataOffset;
+ private readonly uint _dataSize;
+
+ public LinkEditCommand(MachLoadCommandType command, uint dataOffset, uint dataSize, MachHeader header)
+ {
+ _command = (MachLoadCommandType)header.ConvertValue((uint)command);
+ uint commandSize;
+ unsafe { commandSize = (uint) sizeof(LinkEditCommand); }
+ _commandSize = header.ConvertValue(commandSize);
+ _dataOffset = header.ConvertValue(dataOffset);
+ _dataSize = header.ConvertValue(dataSize);
+ }
+
+ public bool IsDefault => this.Equals(default(LinkEditCommand));
+
+ internal uint GetDataOffset(MachHeader header) => header.ConvertValue(_dataOffset);
+ internal uint GetFileSize(MachHeader header) => header.ConvertValue(_dataSize);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LoadCommand.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LoadCommand.cs
new file mode 100644
index 0000000000000..c2ff7988d2ea6
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/LoadCommand.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// The base structure for all load commands in a Mach-O binary.
+/// Load commands are used to describe the structure of the binary.
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/include/mach-o/loader.h#L265 for reference;
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct LoadCommand
+{
+ private readonly MachLoadCommandType _command;
+ private readonly uint _commandSize;
+ public MachLoadCommandType GetCommandType(MachHeader header) => (MachLoadCommandType)header.ConvertValue((uint)_command);
+ public uint GetCommandSize(MachHeader header) => header.ConvertValue(_commandSize);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/MachHeader.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/MachHeader.cs
new file mode 100644
index 0000000000000..b2fd21a5eb1dd
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/MachHeader.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// The Mach-O header is the first data in a Mach-O file.
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/include/mach-o/loader.h#L80 for reference.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct MachHeader
+{
+ private readonly MachMagic _magic;
+ private readonly uint _cpuType;
+ private readonly uint _cpuSubType;
+ private readonly MachFileType _fileType;
+ private uint _numberOfCommands;
+ private uint _sizeOfCommands;
+ private readonly uint _flags;
+ private readonly uint reserved;
+
+ public uint NumberOfCommands { get => _magic.ConvertValue(_numberOfCommands); set => _numberOfCommands = _magic.ConvertValue(value); }
+ public uint SizeOfCommands { get => _magic.ConvertValue(_sizeOfCommands); set => _sizeOfCommands = _magic.ConvertValue(value); }
+ public bool Is64Bit => _magic is MachMagic.MachHeader64CurrentEndian or MachMagic.MachHeader64OppositeEndian;
+ public MachFileType FileType => (MachFileType)_magic.ConvertValue((uint)_fileType);
+
+ public uint ConvertValue(uint value) => _magic.ConvertValue(value);
+ public ulong ConvertValue(ulong value) => _magic.ConvertValue(value);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/NameBuffer.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/NameBuffer.cs
new file mode 100644
index 0000000000000..37e188e9e9f72
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/NameBuffer.cs
@@ -0,0 +1,37 @@
+// 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.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// A 16 byte buffer used to store names in Mach-O load commands.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct NameBuffer
+{
+ private ulong _nameLower;
+ private ulong _nameUpper;
+
+ private NameBuffer(ReadOnlySpan nameBytes)
+ {
+ byte[] buffer = new byte[16];
+ nameBytes.CopyTo(buffer);
+
+ if (BitConverter.IsLittleEndian)
+ {
+ _nameLower = BitConverter.ToUInt64(buffer, 0);
+ _nameUpper = BitConverter.ToUInt64(buffer, 8);
+ }
+ else
+ {
+ _nameLower = BitConverter.ToUInt64(buffer, 8);
+ _nameUpper = BitConverter.ToUInt64(buffer, 0);
+ }
+ }
+
+ public static NameBuffer __TEXT = new NameBuffer("__TEXT"u8);
+ public static NameBuffer __LINKEDIT = new NameBuffer("__LINKEDIT"u8);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/RequirementsBlob.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/RequirementsBlob.cs
new file mode 100644
index 0000000000000..3ba8fb7c7aff1
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/RequirementsBlob.cs
@@ -0,0 +1,50 @@
+// 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.Buffers.Binary;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/requirement.h#L211
+/// Code signature data is always big endian / network order.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct RequirementsBlob
+{
+ private BlobMagic _magic;
+ private uint _size;
+ private uint _subBlobCount;
+
+ public static RequirementsBlob Empty = GetEmptyRequirementsBlob();
+
+ public byte[] GetBytes()
+ {
+ Debug.Assert(_subBlobCount == 0);
+ byte[] buffer = new byte[12];
+ if (BitConverter.IsLittleEndian)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)_magic);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(sizeof(uint)), _size);
+ BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(sizeof(uint) + sizeof(uint)), _subBlobCount);
+ return buffer;
+ }
+ BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)_magic);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(sizeof(uint)), _size);
+ BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(sizeof(uint) + sizeof(uint)), _subBlobCount);
+ return buffer;
+ }
+
+ private static unsafe RequirementsBlob GetEmptyRequirementsBlob()
+ {
+ return new RequirementsBlob
+ {
+ _magic = (BlobMagic)((uint)BlobMagic.Requirements).ConvertToBigEndian(),
+ _size = ((uint)sizeof(RequirementsBlob)).ConvertToBigEndian(),
+ _subBlobCount = 0
+ };
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Section64LoadCommand.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Section64LoadCommand.cs
new file mode 100644
index 0000000000000..f2cb79b243c9e
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Section64LoadCommand.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// A load command that provides information about a section in a segment.
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/include/mach-o/loader.h#L468 for reference.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal readonly struct Section64LoadCommand
+{
+ private readonly NameBuffer _sectionName;
+ private readonly NameBuffer _segmentName;
+ private readonly ulong _address;
+ private readonly ulong _size;
+ private readonly uint _fileOffset;
+ private readonly uint _log2Alignment;
+ private readonly uint _relocationOffset;
+ private readonly uint _numberOfReloationEntries;
+ private readonly uint _flags;
+ private readonly uint _reserved1;
+ private readonly uint _reserved2;
+ private readonly uint _reserved3;
+
+ internal uint GetFileOffset(MachHeader header) => header.ConvertValue(_fileOffset);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Segment64LoadCommand.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Segment64LoadCommand.cs
new file mode 100644
index 0000000000000..d6a49533af995
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/BinaryFormat/Segment64LoadCommand.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// The structure for the 64-bit segment load command in a Mach-O binary.
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/include/mach-o/loader.h#L394 for reference.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct Segment64LoadCommand
+{
+ private readonly MachLoadCommandType _command;
+ private readonly uint _commandSize;
+ public NameBuffer Name;
+ private readonly ulong _address;
+ private ulong _size;
+ private readonly ulong _fileOffset;
+ private ulong _fileSize;
+ private readonly uint _maximumProtection;
+ private readonly uint _initialProtection;
+ private readonly uint _numberOfSections;
+ private readonly uint _flags;
+
+ public bool IsDefault => this.Equals(default(Segment64LoadCommand));
+
+ public ulong GetFileOffset(MachHeader header) => header.ConvertValue(_fileOffset);
+ public ulong GetFileSize(MachHeader header) => header.ConvertValue(_fileSize);
+ public void SetFileSize(ulong value, MachHeader header) => _fileSize = header.ConvertValue(value);
+ public void SetVMSize(ulong value, MachHeader header) => _size = header.ConvertValue(value);
+ public uint GetSectionsCount(MachHeader header) => header.ConvertValue(_numberOfSections);
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/EndianConversionExtensions.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/EndianConversionExtensions.cs
new file mode 100644
index 0000000000000..1f347760cfb29
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/EndianConversionExtensions.cs
@@ -0,0 +1,29 @@
+// 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.Buffers.Binary;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+public static class EndianConversionExtensions
+{
+ public static uint ConvertToBigEndian(this uint value)
+ {
+ return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(value) : value;
+ }
+ public static ulong ConvertToBigEndian(this ulong value)
+ {
+ return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(value) : value;
+ }
+
+ public static uint ConvertFromBigEndian(this uint value)
+ {
+ return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(value) : value;
+ }
+
+ public static ulong ConvertFromBigEndian(this ulong value)
+ {
+ return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(value) : value;
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs
new file mode 100644
index 0000000000000..a559fdc4fd96c
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/BlobMagic.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/cscdefs.h#L4
+///
+internal enum BlobMagic : uint
+{
+ Requirements = 0xfade0c01,
+ CodeDirectory = 0xfade0c02,
+ EmbeddedSignature = 0xfade0cc0,
+ CmsWrapper = 0xfade0b01,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryFlags.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryFlags.cs
new file mode 100644
index 0000000000000..fd2cb3d331489
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryFlags.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/CSCommon.h#L288 for reference.
+///
+internal enum CodeDirectoryFlags : uint
+{
+ Adhoc = 2,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs
new file mode 100644
index 0000000000000..ea2076d9b5e46
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectorySpecialSlot.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See
+///
+internal enum CodeDirectorySpecialSlot
+{
+ CodeDirectory = 0,
+ Requirements = 2,
+ CmsWrapper = 0x10000,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryVersion.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryVersion.cs
new file mode 100644
index 0000000000000..bd8eef4af6796
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/CodeDirectoryVersion.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/codedirectory.h#L222-L227
+///
+internal enum CodeDirectoryVersion : int
+{
+ Baseline = 0x20001,
+ SupportsScatter = 0x20100,
+ SupportsTeamId = 0x20200,
+ SupportsCodeLimit64 = 0x20300,
+ SupportsExecSegment = 0x20400,
+ SupportsPreEncrypt = 0x20500,
+ HighestVersion = SupportsExecSegment, // TODO: We don't support pre-encryption yet
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/ExecutableSegmentFlags.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/ExecutableSegmentFlags.cs
new file mode 100644
index 0000000000000..22f22c0b1c7fb
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/ExecutableSegmentFlags.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/CSCommonPriv.h#L96 for reference.
+///
+internal enum ExecutableSegmentFlags : ulong
+{
+ MainBinary = 1,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/HashType.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/HashType.cs
new file mode 100644
index 0000000000000..c3d49d9969577
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/HashType.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/Security/blob/3dab46a11f45f2ffdbd70e2127cc5a8ce4a1f222/OSX/libsecurity_codesigning/lib/CSCommon.h#L384 for reference.
+///
+internal enum HashType : byte
+{
+ SHA256 = 2,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachFileType.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachFileType.cs
new file mode 100644
index 0000000000000..711b48597ab41
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachFileType.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+internal enum MachFileType : uint
+{
+ Execute = 2,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachLoadCommandType.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachLoadCommandType.cs
new file mode 100644
index 0000000000000..b827f6a2f7e7e
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachLoadCommandType.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/include/mach-o/loader.h#L282 for reference.
+///
+internal enum MachLoadCommandType : uint
+{
+ Segment = 0x1,
+ SymbolTable = 0x2,
+ Segment64 = 0x19,
+ CodeSignature = 0x1d,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachMagic.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachMagic.cs
new file mode 100644
index 0000000000000..f36766cf58e41
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/Enums/MachMagic.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// See https://github.com/apple-oss-distributions/cctools/blob/7a5450708479bbff61527d5e0c32a3f7b7e4c1d0/tests/include/MachO/MachHeader.pm#L12 for definitions.
+///
+internal enum MachMagic : uint
+{
+ MachHeaderOppositeEndian = 0xcefaedfe,
+ MachHeaderCurrentEndian = 0xfeedface,
+ MachHeader64OppositeEndian = 0xcffaedfe,
+ MachHeader64CurrentEndian = 0xfeedfacf,
+ FatMagicOppositeEndian = 0xbebafeca,
+ FatMagicCurrentEndian = 0xcafebabe,
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachMagicExtensions.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachMagicExtensions.cs
new file mode 100644
index 0000000000000..2235664e195f0
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachMagicExtensions.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers.Binary;
+using System.IO;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+internal static class MachMagicExtensions
+{
+ public static uint ConvertValue(this MachMagic magic, uint value)
+ {
+ return magic switch
+ {
+ MachMagic.MachHeader64CurrentEndian or MachMagic.MachHeaderCurrentEndian
+ => value,
+ MachMagic.MachHeader64OppositeEndian or MachMagic.MachHeaderOppositeEndian
+ => BinaryPrimitives.ReverseEndianness(value),
+ _ => throw new InvalidDataException($"Invalid magic value 0x{magic:X}")
+ };
+ }
+
+ public static ulong ConvertValue(this MachMagic magic, ulong value)
+ {
+ return magic switch
+ {
+ MachMagic.MachHeader64CurrentEndian or MachMagic.MachHeaderCurrentEndian
+ => value,
+ MachMagic.MachHeader64OppositeEndian or MachMagic.MachHeaderOppositeEndian
+ => BinaryPrimitives.ReverseEndianness(value),
+ _ => throw new InvalidDataException($"Invalid magic value 0x{magic:X}")
+ };
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.CodeSignature.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.CodeSignature.cs
new file mode 100644
index 0000000000000..fe970a3c4a55f
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.CodeSignature.cs
@@ -0,0 +1,274 @@
+// 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.Diagnostics;
+using System.IO;
+using System.IO.MemoryMappedFiles;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// Managed class with information about a Mach-O code signature.
+///
+internal unsafe partial class MachObjectFile
+{
+ private class CodeSignature
+ {
+ private const uint SpecialSlotCount = 2;
+ private const uint PageSize = 4096;
+ private const byte Log2PageSize = 12;
+ private const byte DefaultHashSize = 32;
+ private const HashType DefaultHashType = HashType.SHA256;
+ private static IncrementalHash GetDefaultIncrementalHash() => IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
+
+ private readonly long _fileOffset;
+ private EmbeddedSignatureHeader _embeddedSignature;
+ private CodeDirectoryHeader _codeDirectoryHeader;
+ private byte[] _identifier;
+ private byte[] _codeDirectoryHashes;
+ private RequirementsBlob _requirementsBlob;
+ private CmsWrapperBlob _cmsWrapperBlob;
+ private bool _unrecognizedFormat;
+
+ private CodeSignature(long fileOffset) { _fileOffset = fileOffset; }
+
+ ///
+ /// Creates a new code signature from the file.
+ /// The signature is composed of an Embedded Signature Superblob header, followed by a CodeDirectory blob, a Requirements blob, and a CMS blob.
+ /// The codesign tool also adds an empty Requirements blob and an empty CMS blob, which are not strictly required but are added here for compatibility.
+ ///
+ internal static CodeSignature CreateSignature(MachObjectFile machObject, MemoryMappedViewAccessor file, string identifier)
+ {
+ uint signatureStart = machObject.GetSignatureStart();
+ EmbeddedSignatureHeader embeddedSignature = new();
+ CodeDirectoryHeader codeDirectory = CreateCodeDirectoryHeader(machObject, signatureStart, identifier);
+ RequirementsBlob requirementsBlob = RequirementsBlob.Empty;
+ CmsWrapperBlob cmsWrapperBlob = CmsWrapperBlob.Empty;
+
+ byte[] identifierBytes = new byte[GetIdentifierLength(identifier)];
+ Encoding.UTF8.GetBytes(identifier).CopyTo(identifierBytes, 0);
+
+ byte[] codeDirectoryHashes = new byte[(GetCodeSlotCount(signatureStart) + SpecialSlotCount) * DefaultHashSize];
+
+ // Fill in the CodeDirectory hashes
+ {
+ var hasher = GetDefaultIncrementalHash();
+
+ // Special slot hashes
+ int hashSlotsOffset = 0;
+ // -2 is the requirements blob hash
+ hasher.AppendData(requirementsBlob.GetBytes());
+ byte[] hash = hasher.GetHashAndReset();
+ Debug.Assert(hash.Length == DefaultHashSize);
+ hash.CopyTo(codeDirectoryHashes, hashSlotsOffset);
+ hashSlotsOffset += DefaultHashSize;
+ // -1 is the CMS blob hash (which is empty -- nothing to hash)
+ hashSlotsOffset += DefaultHashSize;
+
+ // 0 - N are Code hashes
+ byte[] pageBuffer = new byte[(int)PageSize];
+ long remaining = signatureStart;
+ long buffptr = 0;
+ while (remaining > 0)
+ {
+ int codePageSize = (int)Math.Min(remaining, 4096);
+ int bytesRead = file.ReadArray(buffptr, pageBuffer, 0, codePageSize);
+ if (bytesRead != codePageSize)
+ throw new IOException("Could not read all bytes");
+ buffptr += bytesRead;
+ hasher.AppendData(pageBuffer, 0, codePageSize);
+ hash = hasher.GetHashAndReset();
+ Debug.Assert(hash.Length == DefaultHashSize);
+ hash.CopyTo(codeDirectoryHashes, hashSlotsOffset);
+ remaining -= codePageSize;
+ hashSlotsOffset += DefaultHashSize;
+ }
+ }
+
+ // Create Embedded Signature Header
+ embeddedSignature.Size = GetCodeSignatureSize(signatureStart, identifier);
+ embeddedSignature.CodeDirectory = new BlobIndex(
+ CodeDirectorySpecialSlot.CodeDirectory,
+ (uint)sizeof(EmbeddedSignatureHeader));
+ embeddedSignature.Requirements = new BlobIndex(
+ CodeDirectorySpecialSlot.Requirements,
+ (uint)sizeof(EmbeddedSignatureHeader)
+ + GetCodeDirectorySize(signatureStart, identifier));
+ embeddedSignature.CmsWrapper = new BlobIndex(
+ CodeDirectorySpecialSlot.CmsWrapper,
+ (uint)sizeof(EmbeddedSignatureHeader)
+ + GetCodeDirectorySize(signatureStart, identifier)
+ + (uint)sizeof(RequirementsBlob));
+
+ return new CodeSignature(signatureStart)
+ {
+ _embeddedSignature = embeddedSignature,
+ _codeDirectoryHeader = codeDirectory,
+ _identifier = identifierBytes,
+ _codeDirectoryHashes = codeDirectoryHashes,
+ _requirementsBlob = requirementsBlob,
+ _cmsWrapperBlob = cmsWrapperBlob
+ };
+ }
+
+ internal static uint GetCodeSignatureSize(uint signatureStart, string identifier)
+ {
+ return (uint)(sizeof(EmbeddedSignatureHeader)
+ + GetCodeDirectorySize(signatureStart, identifier)
+ + sizeof(RequirementsBlob)
+ + sizeof(CmsWrapperBlob));
+ }
+
+ internal static CodeSignature Read(MemoryMappedViewAccessor file, long fileOffset)
+ {
+ CodeSignature cs = new CodeSignature(fileOffset);
+ file.Read(fileOffset, out cs._embeddedSignature);
+ if (cs._embeddedSignature.BlobCount != 3
+ || cs._embeddedSignature.CodeDirectory.Slot != CodeDirectorySpecialSlot.CodeDirectory
+ || cs._embeddedSignature.Requirements.Slot != CodeDirectorySpecialSlot.Requirements
+ || cs._embeddedSignature.CmsWrapper.Slot != CodeDirectorySpecialSlot.CmsWrapper)
+ {
+ cs._unrecognizedFormat = true;
+ return cs;
+ }
+ var cdOffset = cs._fileOffset + cs._embeddedSignature.CodeDirectory.Offset;
+ file.Read(cdOffset, out cs._codeDirectoryHeader);
+ if (cs._codeDirectoryHeader.Version != CodeDirectoryVersion.HighestVersion
+ || cs._codeDirectoryHeader.HashType != HashType.SHA256
+ || cs._codeDirectoryHeader.SpecialSlotCount != SpecialSlotCount)
+ {
+ cs._unrecognizedFormat = true;
+ return cs;
+ }
+
+ long identifierOffset = cdOffset + cs._codeDirectoryHeader.IdentifierOffset;
+ long codeHashesOffset = cdOffset + cs._codeDirectoryHeader.HashesOffset - (SpecialSlotCount * DefaultHashSize);
+
+ cs._identifier = new byte[codeHashesOffset - identifierOffset];
+ file.ReadArray(identifierOffset, cs._identifier, 0, cs._identifier.Length);
+
+ cs._codeDirectoryHashes = new byte[(SpecialSlotCount + cs._codeDirectoryHeader.CodeSlotCount) * DefaultHashSize];
+ file.ReadArray(codeHashesOffset, cs._codeDirectoryHashes, 0, cs._codeDirectoryHashes.Length);
+
+ var requirementsOffset = cs._fileOffset + cs._embeddedSignature.Requirements.Offset;
+ file.Read(requirementsOffset, out cs._requirementsBlob);
+ if (!cs._requirementsBlob.Equals(RequirementsBlob.Empty))
+ {
+ cs._unrecognizedFormat = true;
+ return cs;
+ }
+
+ var cmsOffset = fileOffset + cs._embeddedSignature.CmsWrapper.Offset;
+ file.Read(cmsOffset, out cs._cmsWrapperBlob);
+ if (!cs._cmsWrapperBlob.Equals(CmsWrapperBlob.Empty))
+ {
+ cs._unrecognizedFormat = true;
+ return cs;
+ }
+ return cs;
+ }
+
+ internal void WriteToFile(MemoryMappedViewAccessor file)
+ {
+ long fileOffset = _fileOffset;
+
+ file.Write(fileOffset, ref _embeddedSignature);
+ fileOffset += sizeof(EmbeddedSignatureHeader);
+
+ file.Write(fileOffset, ref _codeDirectoryHeader);
+ fileOffset += sizeof(CodeDirectoryHeader);
+
+ file.WriteArray(fileOffset, _identifier, 0, _identifier.Length);
+ fileOffset += _identifier.Length;
+
+ file.WriteArray(fileOffset, _codeDirectoryHashes, 0, _codeDirectoryHashes.Length);
+ fileOffset += _codeDirectoryHashes.Length;
+
+ file.Write(fileOffset, ref _requirementsBlob);
+ fileOffset += sizeof(RequirementsBlob);
+
+ file.Write(fileOffset, ref _cmsWrapperBlob);
+ Debug.Assert(fileOffset + sizeof(CmsWrapperBlob) == _fileOffset + _embeddedSignature.Size);
+ }
+
+ private static CodeDirectoryHeader CreateCodeDirectoryHeader(MachObjectFile machObject, uint signatureStart, string identifier)
+ {
+ CodeDirectoryVersion version = CodeDirectoryVersion.HighestVersion;
+ uint identifierLength = GetIdentifierLength(identifier);
+ uint codeDirectorySize = GetCodeDirectorySize((uint)signatureStart, identifier);
+
+ CodeDirectoryHeader codeDirectoryBlob = new();
+ uint hashesOffset;
+ hashesOffset = (uint)sizeof(CodeDirectoryHeader) + identifierLength + DefaultHashSize * SpecialSlotCount;
+ codeDirectoryBlob.Size = codeDirectorySize;
+ codeDirectoryBlob.Version = version;
+ codeDirectoryBlob.Flags = CodeDirectoryFlags.Adhoc;
+ codeDirectoryBlob.HashesOffset = hashesOffset;
+ codeDirectoryBlob.IdentifierOffset = (uint)sizeof(CodeDirectoryHeader);
+ codeDirectoryBlob.SpecialSlotCount = SpecialSlotCount;
+ codeDirectoryBlob.CodeSlotCount = GetCodeSlotCount(signatureStart);
+ codeDirectoryBlob.ExecutableLength = signatureStart > uint.MaxValue ? uint.MaxValue : signatureStart;
+ codeDirectoryBlob.HashSize = DefaultHashSize;
+ codeDirectoryBlob.HashType = DefaultHashType;
+ codeDirectoryBlob.Platform = 0;
+ codeDirectoryBlob.Log2PageSize = Log2PageSize;
+
+ codeDirectoryBlob.CodeLimit64 = signatureStart >= uint.MaxValue ? signatureStart : 0;
+ codeDirectoryBlob.ExecSegmentBase = machObject._textSegment64.Command.GetFileOffset(machObject._header);
+ codeDirectoryBlob.ExecSegmentLimit = machObject._textSegment64.Command.GetFileSize(machObject._header);
+ if (machObject._header.FileType == MachFileType.Execute)
+ codeDirectoryBlob.ExecSegmentFlags |= ExecutableSegmentFlags.MainBinary;
+
+ return codeDirectoryBlob;
+ }
+
+ private static uint GetIdentifierLength(string identifier)
+ {
+ return (uint)(Encoding.UTF8.GetByteCount(identifier) + 1);
+ }
+
+ private static uint GetCodeDirectorySize(uint signatureStart, string identifier)
+ {
+ return (uint)(sizeof(CodeDirectoryHeader)
+ + GetIdentifierLength(identifier)
+ + SpecialSlotCount * DefaultHashSize
+ + GetCodeSlotCount(signatureStart) * DefaultHashSize);
+ }
+
+ private static uint GetCodeSlotCount(uint signatureStart)
+ {
+ return (signatureStart + PageSize - 1) / PageSize;
+ }
+
+ public static bool AreEquivalent(CodeSignature a, CodeSignature b)
+ {
+ if (a is null ^ b is null)
+ return false;
+ if (a is null && b is null)
+ return true;
+ if (a._unrecognizedFormat || b._unrecognizedFormat)
+ return false;
+ if (!a._embeddedSignature.Equals(b._embeddedSignature))
+ return false;
+ if (!a._codeDirectoryHeader.Equals(b._codeDirectoryHeader))
+ return false;
+ if (!a._identifier.SequenceEqual(b._identifier))
+ return false;
+
+ var aSpecialSlotHashes = a._codeDirectoryHashes.AsSpan(0, (int)SpecialSlotCount * DefaultHashSize);
+ var bSpecialSlotHashes = b._codeDirectoryHashes.AsSpan(0, (int)SpecialSlotCount * DefaultHashSize);
+ if (!aSpecialSlotHashes.SequenceEqual(bSpecialSlotHashes))
+ return false;
+ var aCodeHashes = a._codeDirectoryHashes.AsSpan(((int)SpecialSlotCount + 1) * DefaultHashSize);
+ var bCodeHashes = b._codeDirectoryHashes.AsSpan(((int)SpecialSlotCount + 1) * DefaultHashSize);
+ if (!aCodeHashes.SequenceEqual(bCodeHashes))
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
new file mode 100644
index 0000000000000..99a822c0c7546
--- /dev/null
+++ b/src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs
@@ -0,0 +1,334 @@
+// 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 System.IO.MemoryMappedFiles;
+using Microsoft.NET.HostModel.AppHost;
+
+namespace Microsoft.NET.HostModel.MachO;
+
+///
+/// A managed object containing relevant information for AdHoc signing a Mach-O file.
+/// The object is created from a memory mapped file, and a signature can be calculated from the memory mapped file.
+/// However, since a memory mapped file cannot be extended, the signature is written to a file stream.
+///
+internal unsafe partial class MachObjectFile
+{
+ private MachHeader _header;
+ private (LinkEditCommand Command, long FileOffset) _codeSignatureLoadCommand;
+ private readonly (Segment64LoadCommand Command, long FileOffset) _textSegment64;
+ private (Segment64LoadCommand Command, long FileOffset) _linkEditSegment64;
+ private CodeSignature _codeSignatureBlob;
+ ///
+ /// The offset of the lowest section in the object file. This is to ensure that additional load commands do not overwrite sections.
+ ///
+ private readonly long _lowestSectionOffset;
+ ///
+ /// The offset in the object file where the next additional load command should be written.
+ ///
+ private readonly long _nextCommandPtr;
+
+ private MachObjectFile(
+ MachHeader header,
+ (LinkEditCommand Command, long FileOffset) codeSignatureLC,
+ (Segment64LoadCommand Command, long FileOffset) textSegment64,
+ (Segment64LoadCommand Command, long FileOffset) linkEditSegment64,
+ long lowestSection,
+ CodeSignature codeSignatureBlob,
+ long nextCommandPtr)
+ {
+ _codeSignatureBlob = codeSignatureBlob;
+ _header = header;
+ _codeSignatureLoadCommand = codeSignatureLC;
+ _textSegment64 = textSegment64;
+ _linkEditSegment64 = linkEditSegment64;
+ _lowestSectionOffset = lowestSection;
+ _nextCommandPtr = nextCommandPtr;
+ }
+
+ ///
+ /// Reads the information from a memory mapped Mach-O file and creates a that represents it.
+ ///
+ public static MachObjectFile Create(MemoryMappedViewAccessor file)
+ {
+ long commandsPtr = 0;
+ if (!IsMachOImage(file))
+ throw new InvalidDataException("File is not a Mach-O image");
+
+ file.Read(commandsPtr, out MachHeader header);
+ if (!header.Is64Bit)
+ throw new AppHostMachOFormatException(MachOFormatError.Not64BitExe);
+
+ long nextCommandPtr = ReadCommands(
+ file,
+ in header,
+ out (LinkEditCommand Command, long FileOffset) codeSignatureLC,
+ out (Segment64LoadCommand Command, long FileOffset) textSegment64,
+ out (Segment64LoadCommand Command, long FileOffset) linkEditSegment64,
+ out long lowestSection);
+ CodeSignature codeSignatureBlob = codeSignatureLC.Command.IsDefault
+ ? null
+ : CodeSignature.Read(file, codeSignatureLC.Command.GetDataOffset(header));
+ return new MachObjectFile(
+ header,
+ codeSignatureLC,
+ textSegment64,
+ linkEditSegment64,
+ lowestSection,
+ codeSignatureBlob,
+ nextCommandPtr);
+ }
+
+ ///
+ /// Returns true if the file has a code signature load command.
+ ///
+ public bool HasSignature => !_codeSignatureLoadCommand.Command.IsDefault;
+
+ ///
+ /// Adds or replaces the code signature load command and modifies the __LINKEDIT segment size to accomodate the signature.
+ /// Writes the EmbeddedSignature blob to the file.
+ /// Returns the new size of the file (the end of the signature blob).
+ ///
+ public long CreateAdHocSignature(MemoryMappedViewAccessor file, string identifier)
+ {
+ AllocateCodeSignatureLoadCommand(identifier);
+ _codeSignatureBlob = null;
+ // The code signature includes hashes of the entire file up to the code signature.
+ // In order to calculate the hashes correctly, everything up to the code signature must be written before the signature is built.
+ Write(file);
+ _codeSignatureBlob = CodeSignature.CreateSignature(this, file, identifier);
+ _codeSignatureBlob.WriteToFile(file);
+ return GetFileSize();
+ }
+
+ public static bool IsMachOImage(MemoryMappedViewAccessor memoryMappedViewAccessor)
+ {
+ memoryMappedViewAccessor.Read(0, out MachMagic magic);
+ return magic is MachMagic.MachHeaderCurrentEndian or MachMagic.MachHeaderOppositeEndian
+ or MachMagic.MachHeader64CurrentEndian or MachMagic.MachHeader64OppositeEndian
+ or MachMagic.FatMagicCurrentEndian or MachMagic.FatMagicOppositeEndian;
+ }
+
+ public static bool IsMachOImage(FileStream file)
+ {
+ long oldPosition = file.Position;
+ file.Position = 0;
+ // We can read the Magic as any endianness since we just need to determine if it is a Mach-O file.
+ uint magic = (uint)(file.ReadByte() << 24 | file.ReadByte() << 16 | file.ReadByte() << 8 | file.ReadByte());
+ file.Position = oldPosition;
+ return (MachMagic)magic is MachMagic.MachHeaderCurrentEndian or MachMagic.MachHeaderOppositeEndian
+ or MachMagic.MachHeader64CurrentEndian or MachMagic.MachHeader64OppositeEndian
+ or MachMagic.FatMagicCurrentEndian or MachMagic.FatMagicOppositeEndian;
+ }
+
+ public static bool IsMachOImage(string filePath)
+ {
+ using (BinaryReader reader = new BinaryReader(File.OpenRead(filePath)))
+ {
+ if (reader.BaseStream.Length < 256) // Header size
+ {
+ return false;
+ }
+ uint magic = reader.ReadUInt32();
+ return Enum.IsDefined(typeof(MachMagic), magic);
+ }
+ }
+
+ ///
+ /// Removes the code signature load command and signature blob from the file if present.
+ /// Returns true and sets to a non-null value if the file is a MachO file and the signature was removed.
+ /// Returns false and sets newLength to null otherwise.
+ ///
+ /// The file to remove the signature from.
+ /// The new length of the file if the signature is remove and the method returns true
+ /// True if a signature was present and removed, false otherwise
+ public static bool TryRemoveCodesign(MemoryMappedViewAccessor memoryMappedViewAccessor, out long? newLength)
+ {
+ newLength = null;
+ if (!IsMachOImage(memoryMappedViewAccessor))
+ return false;
+
+ MachObjectFile machFile = Create(memoryMappedViewAccessor);
+ if (machFile._codeSignatureLoadCommand.Command.IsDefault)
+ return false;
+
+ machFile._header.NumberOfCommands -= 1;
+ machFile._header.SizeOfCommands -= (uint)sizeof(LinkEditCommand);
+ machFile._linkEditSegment64.Command.SetFileSize(
+ machFile._linkEditSegment64.Command.GetFileSize(machFile._header)
+ - machFile._codeSignatureLoadCommand.Command.GetFileSize(machFile._header),
+ machFile._header);
+ newLength = machFile.GetFileSize();
+ machFile._codeSignatureLoadCommand = default;
+ machFile._codeSignatureBlob = null;
+ machFile.Write(memoryMappedViewAccessor);
+ return true;
+ }
+
+ ///
+ /// Returns true if the two signed MachObjectFiles are equivalent.
+ /// Since the entire file isn't store in the object, the code signature is required.
+ /// The __LINKEDIT segment size is allowed to be different since codesign adds additional padding at the end.
+ /// The difference in __LINKEDIT size causes the first page hash to be different, so the first code hash is ignored.
+ ///
+ public static bool AreEquivalent(MachObjectFile a, MachObjectFile b)
+ {
+ if (!a._header.Equals(b._header))
+ return false;
+ if (!CodeSignatureLCsAreEquivalent(a._codeSignatureLoadCommand, b._codeSignatureLoadCommand, a._header))
+ return false;
+ if (!a._textSegment64.Equals(b._textSegment64))
+ return false;
+ if (!LinkEditSegmentsAreEquivalent(a._linkEditSegment64, b._linkEditSegment64, a._header))
+ return false;
+ if (a._codeSignatureBlob is null || b._codeSignatureBlob is null)
+ return false;
+ // This may be false if the __LINKEDIT segment load command is not on the first page, but that is unlikely.
+ if (!CodeSignature.AreEquivalent(a._codeSignatureBlob, b._codeSignatureBlob))
+ return false;
+
+ return true;
+
+ static bool CodeSignatureLCsAreEquivalent((LinkEditCommand Command, long FileOffset) a, (LinkEditCommand Command, long FileOffset) b, MachHeader header)
+ {
+ if (a.Command.GetDataOffset(header) != b.Command.GetDataOffset(header))
+ return false;
+ if (a.FileOffset != b.FileOffset)
+ return false;
+ return true;
+ }
+
+ static bool LinkEditSegmentsAreEquivalent((Segment64LoadCommand Command, long FileOffset) a, (Segment64LoadCommand Command, long FileOffset) b, MachHeader header)
+ {
+ if (a.Command.GetFileOffset(header) != b.Command.GetFileOffset(header))
+ return false;
+ if (a.Command.GetSectionsCount(header) != b.Command.GetSectionsCount(header))
+ return false;
+ if (a.FileOffset != b.FileOffset)
+ return false;
+ return true;
+ }
+ }
+
+ public static long GetSignatureSizeEstimate(uint fileSize, string identifier)
+ {
+ return CodeSignature.GetCodeSignatureSize(fileSize, identifier);
+ }
+
+ ///
+ /// Writes the entire file to .
+ ///
+ private long Write(MemoryMappedViewAccessor file)
+ {
+ if (file.Capacity < GetFileSize())
+ throw new ArgumentException("File is too small", nameof(file));
+ file.Write(0, ref _header);
+ file.Write(_linkEditSegment64.FileOffset, ref _linkEditSegment64.Command);
+ if (!_codeSignatureLoadCommand.Command.IsDefault)
+ {
+ file.Write(_codeSignatureLoadCommand.FileOffset, ref _codeSignatureLoadCommand.Command);
+ _codeSignatureBlob?.WriteToFile(file);
+ }
+ return GetFileSize();
+ }
+
+ ///
+ /// Returns a pointer to the end of the commands list.
+ /// Fills the content of the commands with the corresponding command if present in the file.
+ ///
+ private static long ReadCommands(
+ MemoryMappedViewAccessor inputFile,
+ in MachHeader header,
+ out (LinkEditCommand Command, long FileOffset) codeSignatureLC,
+ out (Segment64LoadCommand Command, long FileOffset) textSegment64,
+ out (Segment64LoadCommand Command, long FileOffset) linkEditSegment64,
+ out long lowestSectionOffset)
+ {
+ codeSignatureLC = default;
+ textSegment64 = default;
+ linkEditSegment64 = default;
+ long commandsPtr;
+ commandsPtr = sizeof(MachHeader);
+ lowestSectionOffset = long.MaxValue;
+ for (int i = 0; i < header.NumberOfCommands; i++)
+ {
+ inputFile.Read(commandsPtr, out LoadCommand loadCommand);
+ switch (loadCommand.GetCommandType(header))
+ {
+ case MachLoadCommandType.CodeSignature:
+ inputFile.Read(commandsPtr, out LinkEditCommand leCommand);
+ codeSignatureLC = (leCommand, commandsPtr);
+ break;
+ case MachLoadCommandType.Segment64:
+ inputFile.Read(commandsPtr, out Segment64LoadCommand segment64);
+ if (segment64.Name.Equals(NameBuffer.__TEXT))
+ {
+ textSegment64 = (segment64, commandsPtr);
+ long sectionPtr = commandsPtr + sizeof(Segment64LoadCommand);
+ uint sectionsCount = segment64.GetSectionsCount(header);
+ for (int s = 0; s < sectionsCount; s++)
+ {
+ inputFile.Read(sectionPtr, out Section64LoadCommand section);
+ lowestSectionOffset = Math.Min(lowestSectionOffset, section.GetFileOffset(header));
+ sectionPtr += sizeof(Section64LoadCommand);
+ }
+ break;
+ }
+ if (segment64.Name.Equals(NameBuffer.__LINKEDIT))
+ {
+ linkEditSegment64 = (segment64, commandsPtr);
+ break;
+ }
+ break;
+ }
+ commandsPtr += loadCommand.GetCommandSize(header);
+ }
+ return commandsPtr;
+ }
+
+ ///
+ /// Clears the old signature and sets the codeSignatureLC to the proper size and offset for a new signature.
+ ///
+ private void AllocateCodeSignatureLoadCommand(string identifier)
+ {
+ uint csOffset = GetSignatureStart();
+ uint csPtr = (uint)(_codeSignatureLoadCommand.Command.IsDefault ? _nextCommandPtr : _codeSignatureLoadCommand.FileOffset);
+ uint csSize = CodeSignature.GetCodeSignatureSize(GetSignatureStart(), identifier);;
+
+ if (_codeSignatureLoadCommand.Command.IsDefault)
+ {
+ // Update the header to accomodate the new code signature load command
+ _header.NumberOfCommands += 1;
+ _header.SizeOfCommands += (uint)sizeof(LinkEditCommand);
+ if (_header.SizeOfCommands > _lowestSectionOffset)
+ {
+ throw new InvalidOperationException("Mach Object does not have enough space for the code signature load command");
+ }
+ }
+
+ var currentLinkEditOffset = _linkEditSegment64.Command.GetFileOffset(_header);
+ var linkEditSize = csOffset + csSize - currentLinkEditOffset;
+ _linkEditSegment64.Command.SetFileSize(linkEditSize, _header);
+ _codeSignatureLoadCommand = (new LinkEditCommand(MachLoadCommandType.CodeSignature, csOffset, csSize, _header), csPtr);
+ }
+
+ ///
+ /// The offset in the file where the code signature starts.
+ /// The signature includes hashes of all bytes up to this offset.
+ ///
+ private uint GetSignatureStart()
+ {
+ if (!_codeSignatureLoadCommand.Command.IsDefault)
+ {
+ return _codeSignatureLoadCommand.Command.GetDataOffset(_header);
+ }
+ return (uint)(_linkEditSegment64.Command.GetFileOffset(_header) + _linkEditSegment64.Command.GetFileSize(_header));
+ }
+
+ ///
+ /// Gets the total size of the Mach-O file according to the load commands.
+ ///
+ private long GetFileSize()
+ => (long)(_linkEditSegment64.Command.GetFileOffset(_header) + _linkEditSegment64.Command.GetFileSize(_header));
+}
diff --git a/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj b/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj
index 9f58898b58295..72004ef3ed083 100644
--- a/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj
+++ b/src/installer/managed/Microsoft.NET.HostModel/Microsoft.NET.HostModel.csproj
@@ -23,6 +23,7 @@
+
@@ -31,9 +32,7 @@
-
-
diff --git a/src/installer/pkg/THIRD-PARTY-NOTICES.TXT b/src/installer/pkg/THIRD-PARTY-NOTICES.TXT
index f1c3b9cfad546..d4ec4333fbfc0 100644
--- a/src/installer/pkg/THIRD-PARTY-NOTICES.TXT
+++ b/src/installer/pkg/THIRD-PARTY-NOTICES.TXT
@@ -1331,7 +1331,7 @@ https://github.com/Microsoft/MSBuildLocator
Copyright (c) 2018 .NET Foundation and Contributors
-This software is licensed subject to the MIT license, available at
+This software is licensed subject to the MIT license, available at
https://opensource.org/licenses/MIT
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
@@ -1391,7 +1391,7 @@ https://github.com/Microsoft/RoslynClrHeapAllocationAnalyzer
Copyright (c) 2018 Microsoft Corporation
-Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
diff --git a/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs b/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
new file mode 100644
index 0000000000000..18752f5f175fa
--- /dev/null
+++ b/src/installer/tests/HostActivation.Tests/MachOHostSigningTests.cs
@@ -0,0 +1,35 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Xunit;
+using FluentAssertions;
+using System;
+using System.IO;
+using Microsoft.DotNet.CoreSetup.Test;
+using Microsoft.DotNet.Cli.Build.Framework;
+using Microsoft.NET.HostModel.AppHost;
+
+namespace HostActivation.Tests
+{
+ public class MachOHostSigningTests
+ {
+ [Fact]
+ [PlatformSpecific(TestPlatforms.OSX)]
+ public void SignedAppHostRuns()
+ {
+ using var testDirectory = TestArtifact.Create(nameof(SignedAppHostRuns));
+ var testAppHostPath = Path.Combine(testDirectory.Location, Path.GetFileName(Binaries.AppHost.FilePath));
+ File.Copy(Binaries.AppHost.FilePath, testAppHostPath);
+ long preRemovalSize = new FileInfo(testAppHostPath).Length;
+ string signedHostPath = testAppHostPath + ".signed";
+
+ HostWriter.CreateAppHost(testAppHostPath, signedHostPath, testAppHostPath + ".dll", enableMacOSCodeSign: true);
+
+ var executedCommand = Command.Create(testAppHostPath)
+ .CaptureStdErr()
+ .CaptureStdOut()
+ .Execute();
+ executedCommand.Should().ExitWith(Constants.ErrorCode.AppHostExeNotBoundFailure);
+ }
+ }
+}
diff --git a/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs b/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs
index 0ebb0912af4f3..1f733682feb78 100644
--- a/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs
+++ b/src/installer/tests/HostActivation.Tests/SelfContainedAppLaunch.cs
@@ -3,7 +3,7 @@
using System;
using System.IO;
-
+using System.Runtime.InteropServices;
using FluentAssertions;
using Microsoft.DotNet.Cli.Build.Framework;
using Microsoft.DotNet.CoreSetup.Test;
@@ -122,7 +122,8 @@ public void RelativeEmbeddedPath()
HostWriter.CreateAppHost(
Binaries.AppHost.FilePath,
appExe,
- Path.GetRelativePath(subDir, app.AppDll));
+ Path.GetRelativePath(subDir, app.AppDll),
+ enableMacOSCodeSign: RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
Command.Create(appExe)
.CaptureStdErr()
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
index c0982c8a187e2..fd3934d6e1352 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs
@@ -6,14 +6,20 @@
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
-using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
using System.Text;
using FluentAssertions;
+using Microsoft.NET.HostModel.MachO.CodeSign;
+using Microsoft.NET.HostModel.MachO;
+using Microsoft.DotNet.Cli.Build.Framework;
+using Microsoft.DotNet.CoreSetup;
using Microsoft.DotNet.CoreSetup.Test;
using Xunit;
+using System.Buffers.Binary;
+using System.IO.MemoryMappedFiles;
+using Microsoft.NET.HostModel.MachO.CodeSign.Tests;
namespace Microsoft.NET.HostModel.AppHost.Tests
{
@@ -255,18 +261,17 @@ public void ExecutableImage()
}
[Theory]
- [PlatformSpecific(TestPlatforms.OSX)]
[InlineData("")]
[InlineData("dir with spaces")]
- public void CodeSignAppHostOnMacOS(string subdir)
+ [PlatformSpecific(TestPlatforms.OSX)]
+ public void CodeSignMachOAppHost(string subdir)
{
using (TestArtifact artifact = CreateTestDirectory())
{
string testDirectory = Path.Combine(artifact.Location, subdir);
Directory.CreateDirectory(testDirectory);
- string sourceAppHostMock = PrepareAppHostMockFile(testDirectory);
- File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly);
- string destinationFilePath = Path.Combine(testDirectory, "DestinationAppHost.exe.mock");
+ string sourceAppHostMock = Binaries.AppHost.FilePath;
+ string destinationFilePath = Path.Combine(testDirectory, Binaries.AppHost.FileName);
string appBinaryFilePath = "Test/App/Binary/Path.dll";
HostWriter.CreateAppHost(
sourceAppHostMock,
@@ -275,6 +280,11 @@ public void CodeSignAppHostOnMacOS(string subdir)
windowsGraphicalUserInterface: false,
enableMacOSCodeSign: true);
+ // Validate that there is a signature present in the apphost Mach file
+ SigningTests.IsSigned(destinationFilePath).Should().BeTrue();
+
+ // Verify with codesign as well
+ Assert.True(Codesign.IsAvailable);
const string codesign = @"/usr/bin/codesign";
var psi = new ProcessStartInfo()
{
@@ -293,28 +303,40 @@ public void CodeSignAppHostOnMacOS(string subdir)
Assert.True(p.ExitCode == 0, $"Expected exit code was '0' but '{codesign}' returned '{p.ExitCode}' instead.");
}
}
+
}
- [Fact]
- [PlatformSpecific(TestPlatforms.OSX)]
- public void DoesNotCodeSignAppHostByDefault()
+ [Theory]
+ [InlineData("")]
+ [InlineData("dir with spaces")]
+ public void CodeSignMockMachOAppHost(string subdir)
{
using (TestArtifact artifact = CreateTestDirectory())
{
- string sourceAppHostMock = PrepareAppHostMockFile(artifact.Location);
- File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly);
- string destinationFilePath = Path.Combine(artifact.Location, "DestinationAppHost.exe.mock");
+ string testDirectory = Path.Combine(artifact.Location, subdir);
+ Directory.CreateDirectory(testDirectory);
+ string sourceAppHostMock = PrepareMockMachAppHostFile(testDirectory);
+ string destinationFilePath = Path.Combine(testDirectory, "DestinationAppHost.exe.mock");
string appBinaryFilePath = "Test/App/Binary/Path.dll";
HostWriter.CreateAppHost(
sourceAppHostMock,
destinationFilePath,
appBinaryFilePath,
- windowsGraphicalUserInterface: false);
+ windowsGraphicalUserInterface: false,
+ enableMacOSCodeSign: true);
+ // Validate that there is a signature present in the apphost Mach file
+ SigningTests.IsSigned(destinationFilePath).Should().BeTrue();
+
+ // Verify with codesign as well
+ if (!Codesign.IsAvailable)
+ {
+ return;
+ }
const string codesign = @"/usr/bin/codesign";
var psi = new ProcessStartInfo()
{
- Arguments = $"-d {destinationFilePath}",
+ Arguments = $"-d \"{destinationFilePath}\"",
FileName = codesign,
RedirectStandardError = true,
};
@@ -323,19 +345,20 @@ public void DoesNotCodeSignAppHostByDefault()
{
p.Start();
p.StandardError.ReadToEnd()
- .Should().Contain($"{Path.GetFullPath(destinationFilePath)}: code object is not signed at all");
+ .Should().Contain($"Executable={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 CodeSigningFailuresThrow()
+ public void DoesNotCodeSignAppHostByDefault()
{
using (TestArtifact artifact = CreateTestDirectory())
{
- string sourceAppHostMock = PrepareAppHostMockFile(artifact.Location);
+ string sourceAppHostMock = PrepareMockMachAppHostFile(artifact.Location);
File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly);
string destinationFilePath = Path.Combine(artifact.Location, "DestinationAppHost.exe.mock");
string appBinaryFilePath = "Test/App/Binary/Path.dll";
@@ -343,20 +366,35 @@ public void CodeSigningFailuresThrow()
sourceAppHostMock,
destinationFilePath,
appBinaryFilePath,
- windowsGraphicalUserInterface: false,
- enableMacOSCodeSign: true);
+ windowsGraphicalUserInterface: false);
+
+ if (!Codesign.IsAvailable)
+ {
+ return;
+ }
- // Run CreateAppHost again to sign the apphost a second time,
- // causing codesign to fail.
- var exception = Assert.Throws(() =>
+ var (exitCode, stdErr) = Codesign.Run("-d", destinationFilePath);
+ stdErr.Should().Contain($"{Path.GetFullPath(destinationFilePath)}: code object is not signed at all");
+ }
+ }
+
+ [Fact]
+ public void CodeSignNotMachOThrows()
+ {
+ using (TestArtifact artifact = CreateTestDirectory())
+ {
+ string sourceAppHostMock = PrepareAppHostMockFile(artifact.Location);
+ File.SetAttributes(sourceAppHostMock, FileAttributes.ReadOnly);
+ string destinationFilePath = Path.Combine(artifact.Location, "DestinationAppHost.exe.mock");
+ string appBinaryFilePath = "Test/App/Binary/Path.dll";
+ // The apphost is not a Mach file, so an exception should be thrown.
+ var exception = Assert.Throws(() =>
HostWriter.CreateAppHost(
sourceAppHostMock,
destinationFilePath,
appBinaryFilePath,
windowsGraphicalUserInterface: false,
enableMacOSCodeSign: true));
- Assert.Contains($"{destinationFilePath}: is already signed", exception.Message);
- Assert.True(exception.ExitCode == 1, $"AppHostSigningException.ExitCode - expected: 1, actual: '{exception.ExitCode}'");
}
}
@@ -447,6 +485,27 @@ private void ResourceWithUnknownLanguage()
}
}
+ private static readonly byte[] s_placeholderData = AppBinaryPathPlaceholderSearchValue.Concat(DotNetSearchPlaceholderValue).ToArray();
+ ///
+ /// Prepares a mock executable file with the AppHost placeholder embedded in it.
+ /// This file will not run, but can be used to test HostWriter and signing process.
+ ///
+ public static string PrepareMockMachAppHostFile(string directory)
+ {
+ string fileName = "MockAppHost.mach.o";
+ string outputFilePath = Path.Combine(directory, fileName);
+ using (var aOutStream = TestData.MachObjects.GetAll().First().File.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var managedSignFile = File.OpenWrite(outputFilePath))
+ {
+ aOutStream!.CopyTo(managedSignFile);
+ // Add the placeholder - it just needs to exist somewhere in the image
+ // We'll put it at 4096 bytes into the file - this should be in the middle of the __TEXT segment
+ managedSignFile.Position = 4096;
+ managedSignFile.Write(s_placeholderData);
+ }
+ return outputFilePath;
+ }
+
private string PrepareAppHostMockFile(string directory, Action customize = null)
{
// For now we're testing the AppHost on Windows PE files only.
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
new file mode 100644
index 0000000000000..6926d5d9a8045
--- /dev/null
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs
@@ -0,0 +1,241 @@
+// 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 System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Microsoft.NET.HostModel.AppHost;
+using Microsoft.NET.HostModel.MachO;
+using Microsoft.DotNet.CoreSetup;
+using Microsoft.DotNet.CoreSetup.Test;
+using Xunit;
+using FluentAssertions;
+using System.IO.MemoryMappedFiles;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.DotNet.Cli.Build.Framework;
+using System.Security.AccessControl;
+
+namespace Microsoft.NET.HostModel.MachO.CodeSign.Tests
+{
+ public class SigningTests
+ {
+ internal static bool IsSigned(string filePath)
+ {
+ // Validate the signature if we can, otherwise, at least ensure there is a signature LoadCommand present
+ if (Codesign.IsAvailable)
+ return Codesign.Run("--verify", filePath).ExitCode == 0;
+
+ using var appHostSourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1);
+ using var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostSourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
+ using var managedSignedAccessor = memoryMappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
+ return MachObjectFile.Create(managedSignedAccessor).HasSignature;
+ }
+
+ static readonly string[] liveBuiltHosts = new string[] { Binaries.AppHost.FilePath, Binaries.SingleFileHost.FilePath };
+ static List GetTestFilePaths(TestArtifact testArtifact)
+ {
+ List<(string Name, FileInfo File)> testData = TestData.MachObjects.GetAll().ToList();
+ List testFilePaths = new();
+ foreach ((string name, FileInfo file) in testData)
+ {
+ string originalFilePath = Path.Combine(testArtifact.Location, name);
+ using (var src = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var dest = new FileStream(originalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
+ {
+ src.CopyTo(dest);
+ }
+ testFilePaths.Add(originalFilePath);
+ }
+
+ // If we're on mac, we can use the live built binaries to test against
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ foreach (var filePath in liveBuiltHosts)
+ {
+ string fileName = Path.GetFileName(filePath);
+ string testFilePath = Path.Combine(testArtifact.Location, fileName);
+ File.Copy(filePath, testFilePath);
+ testFilePaths.Add(testFilePath);
+ }
+ }
+
+ return testFilePaths;
+ }
+
+ [Fact]
+ public void CanSignMachObject()
+ {
+ using var testArtifact = TestArtifact.Create(nameof(CanSignMachObject));
+ foreach (var filePath in GetTestFilePaths(testArtifact))
+ {
+ string fileName = Path.GetFileName(filePath);
+ string originalFilePath = filePath;
+ string managedSignedPath = filePath + ".signed";
+
+ // Managed signed file
+ AdHocSignFile(originalFilePath, managedSignedPath, fileName);
+ Assert.True(IsSigned(managedSignedPath), $"Failed to sign a copy of {filePath}");
+ }
+ }
+
+ [Fact]
+ public void CanRemoveSignature()
+ {
+ using var testArtifact = TestArtifact.Create(nameof(CanRemoveSignature));
+ foreach (var filePath in GetTestFilePaths(testArtifact))
+ {
+ string fileName = Path.GetFileName(filePath);
+ string originalFilePath = filePath;
+ string managedSignedPath = filePath + ".signed";
+ RemoveSignature(originalFilePath, managedSignedPath);
+ Assert.False(IsSigned(managedSignedPath), $"Failed to remove signature from {filePath}");
+ }
+ }
+
+ [Fact]
+ public void CanUnsignAndResign()
+ {
+ using var testArtifact = TestArtifact.Create(nameof(CanUnsignAndResign));
+ foreach (var filePath in GetTestFilePaths(testArtifact))
+ {
+ string fileName = Path.GetFileName(filePath);
+ string originalFilePath = filePath;
+ string managedSignedPath = filePath + ".signed";
+
+ // Managed signed file
+ AdHocSignFile(originalFilePath, managedSignedPath, fileName);
+ Assert.True(IsSigned(managedSignedPath), $"Failed to sign a copy of {filePath}");
+
+ // Remove signature
+ RemoveSignature(managedSignedPath, managedSignedPath + ".unsigned");
+ Assert.False(IsSigned(managedSignedPath + ".unsigned"), $"Failed to remove signature from {filePath}");
+
+ // Resign
+ AdHocSignFile(managedSignedPath + ".unsigned", managedSignedPath + ".resigned", fileName);
+ Assert.True(IsSigned(managedSignedPath + ".resigned"), $"Failed to resign {filePath}");
+ }
+ }
+
+ [Fact]
+ [PlatformSpecific(TestPlatforms.OSX)]
+ void MatchesCodesignOutput()
+ {
+ using var testArtifact = TestArtifact.Create(nameof(MatchesCodesignOutput));
+ foreach (var filePath in GetTestFilePaths(testArtifact))
+ {
+ string fileName = Path.GetFileName(filePath);
+ string originalFilePath = filePath;
+ string codesignFilePath = filePath + ".codesigned";
+ string managedSignedPath = filePath + ".signed";
+
+ // Codesigned file
+ File.Copy(filePath, codesignFilePath);
+ Assert.True(Codesign.IsAvailable, "Could not find codesign tool");
+ Codesign.Run("--remove-signature", codesignFilePath).ExitCode.Should().Be(0, $"'codesign --remove-signature {codesignFilePath}' failed!");
+ Codesign.Run("-s -", codesignFilePath).ExitCode.Should().Be(0, $"'codesign -s - {codesignFilePath}' failed!");
+
+ // Managed signed file
+ AdHocSignFile(originalFilePath, managedSignedPath, fileName);
+
+ var check = Codesign.Run("-v", managedSignedPath);
+ check.ExitCode.Should().Be(0, check.StdErr, $"Failed to sign a copy of '{filePath}'");
+ Assert.True(MachFilesAreEquivalent(codesignFilePath, managedSignedPath, fileName), $"Managed signature does not match codesign output for '{filePath}'");
+ }
+ }
+
+ [Fact]
+ [PlatformSpecific(TestPlatforms.OSX)]
+ void SignedMachOExecutableRuns()
+ {
+ using var testArtifact = TestArtifact.Create(nameof(SignedMachOExecutableRuns));
+ foreach(var (fileName, fileInfo) in TestData.MachObjects.GetRunnable())
+ {
+ string unsignedFilePath = Path.Combine(testArtifact.Location, fileName);
+ string signedPath = unsignedFilePath + ".signed";
+ fileInfo.CopyTo(unsignedFilePath);
+
+ AdHocSignFile(unsignedFilePath, signedPath, fileName);
+
+ // Set the file to be executable
+ File.SetUnixFileMode(signedPath, UnixFileMode.UserRead | UnixFileMode.UserExecute);
+
+ var result = Command.Create(signedPath).CaptureStdErr().CaptureStdOut().Execute();
+ result.ExitCode.Should().Be(0, result.StdErr);
+ }
+ }
+
+ static bool MachFilesAreEquivalent(string codesignedPath, string managedSignedPath, string fileName)
+ {
+ using var managedFileStream = new FileStream(managedSignedPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1);
+ using var managedMMapFile = MemoryMappedFile.CreateFromFile(managedFileStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
+ using var managedSignedAccessor = managedMMapFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
+
+ using var codesignedFileStream = new FileStream(managedSignedPath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1);
+ using var codesignedMMapFile = MemoryMappedFile.CreateFromFile(codesignedFileStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
+ using var codesignedAccessor = codesignedMMapFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.CopyOnWrite);
+
+ var codesignedObject = MachObjectFile.Create(codesignedAccessor);
+ var managedSignedObject = MachObjectFile.Create(managedSignedAccessor);
+ return MachObjectFile.AreEquivalent(codesignedObject, managedSignedObject);
+ }
+
+ ///
+ /// AdHoc sign a test file. This should look similar to HostWriter.CreateAppHost.
+ ///
+ internal static void AdHocSignFile(string originalFilePath, string managedSignedPath, string fileName)
+ {
+ Assert.NotEqual(originalFilePath, managedSignedPath);
+ // Open the source host file.
+ using (FileStream appHostDestinationStream = new FileStream(managedSignedPath, FileMode.Create, FileAccess.ReadWrite))
+ {
+ using (FileStream appHostSourceStream = new(originalFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
+ {
+ appHostSourceStream.CopyTo(appHostDestinationStream);
+ }
+ var appHostLength = appHostDestinationStream.Length;
+ var destinationFileName = Path.GetFileName(managedSignedPath);
+ var appHostSignedLength = appHostLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostLength, destinationFileName);
+
+ using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationStream, null, appHostSignedLength, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
+ using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, appHostSignedLength, MemoryMappedFileAccess.ReadWrite))
+ {
+ var machObjectFile = MachObjectFile.Create(memoryMappedViewAccessor);
+ appHostLength = machObjectFile.CreateAdHocSignature(memoryMappedViewAccessor, fileName);
+ }
+ appHostDestinationStream.SetLength(appHostLength);
+ }
+ }
+
+ ///
+ /// AdHoc sign a test file. This should look similar to HostWriter.CreateAppHost.
+ ///
+ internal static void RemoveSignature(string originalFilePath, string removedSignaturePath)
+ {
+ Assert.NotEqual(originalFilePath, removedSignaturePath);
+ // Open the source host file.
+ using (FileStream appHostDestinationStream = new FileStream(removedSignaturePath, FileMode.Create, FileAccess.ReadWrite))
+ {
+ using (FileStream appHostSourceStream = new(originalFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1))
+ {
+ appHostSourceStream.CopyTo(appHostDestinationStream);
+ }
+ var appHostLength = appHostDestinationStream.Length;
+ var destinationFileName = Path.GetFileName(removedSignaturePath);
+ var appHostSignedLength = appHostLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostLength, destinationFileName);
+
+ using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationStream, null, appHostSignedLength, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
+ using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, appHostSignedLength, MemoryMappedFileAccess.ReadWrite))
+ {
+ if (MachObjectFile.TryRemoveCodesign(memoryMappedViewAccessor, out long? newLength))
+ appHostLength = newLength.Value;
+ }
+ appHostDestinationStream.SetLength(appHostLength);
+ }
+ }
+ }
+}
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/TestData.cs b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/TestData.cs
new file mode 100644
index 0000000000000..ae7c13a5847be
--- /dev/null
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/TestData.cs
@@ -0,0 +1,39 @@
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+internal class TestData
+{
+ internal class MachObjects
+ {
+ internal static IEnumerable<(string Name, FileInfo File)> GetAll()
+ {
+ return GetRecursiveFiles(new DirectoryInfo("MachO"))
+ .Select(f => (f.UniqueName, f.File));
+ }
+
+ // Gets a FileInfo and a unique name for each binary test Mach-O file.
+ // Test data is located at ./MachO// (using the clang/llvm arch names)
+ // Unique name is -
+ private static IEnumerable<(FileInfo File, string UniqueName)> GetRecursiveFiles(DirectoryInfo dir, string prefix = "")
+ {
+ var files = dir.GetFiles().Select(f => (f, $"{prefix}{dir.Name}-{f.Name}".ToLowerInvariant()));
+ var recursiveFiles = dir.GetDirectories().SelectMany(sd => GetRecursiveFiles(sd, $"{prefix}{dir.Name}-"));
+ return files.Concat(recursiveFiles);
+ }
+
+ static readonly string s_currentArchitectureLlvmString = RuntimeInformation.ProcessArchitecture switch {
+ Architecture.X64 => "x86_64",
+ Architecture.X86 => "x86",
+ Architecture.Arm64 => "arm64",
+ _ => throw new PlatformNotSupportedException()
+ };
+ internal static IEnumerable<(string Name, FileInfo File)> GetRunnable()
+ {
+ return GetAll().Where(f => f.Name.Contains(s_currentArchitectureLlvmString));
+ }
+ }
+}
diff --git a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Tests.csproj b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Tests.csproj
index 3172cfcf0099b..053fe5b65fcfb 100644
--- a/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Tests.csproj
+++ b/src/installer/tests/Microsoft.NET.HostModel.Tests/Microsoft.NET.HostModel.Tests.csproj
@@ -13,4 +13,8 @@
+
+
+
+
diff --git a/src/installer/tests/TestUtils/SingleFileTestApp.cs b/src/installer/tests/TestUtils/SingleFileTestApp.cs
index f90af69eaba11..fa4ff517dd23f 100644
--- a/src/installer/tests/TestUtils/SingleFileTestApp.cs
+++ b/src/installer/tests/TestUtils/SingleFileTestApp.cs
@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.IO;
using System.Reflection;
+using System.Runtime.InteropServices;
using Microsoft.NET.HostModel.Bundle;
namespace Microsoft.DotNet.CoreSetup.Test
@@ -96,7 +97,7 @@ public string Bundle(BundleOptions options, out Manifest manifest, Version? bund
bundleDirectory,
options,
targetFrameworkVersion: bundleVersion,
- macosCodesign: true);
+ macosCodesign: RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
// Get all files in the source directory and all sub-directories.
string[] sources = Directory.GetFiles(builtApp.Location, searchPattern: "*", searchOption: SearchOption.AllDirectories);