Skip to content

Commit a538054

Browse files
authored
Use the managed signer to remove the code signature from singlefile bundles (#110063)
Since the bundler uses `codesign` to remove the signature, we end up with an invalid signature for single file osx executables on non-Mac hosts rather than no signature at all. This PR updates the bundler to use the managed signer to remove the signature. Signing bundles requires a little more thought and effort since the headers/load commands need to be updated to include the bundle data in the file. This will be done in a separate PR.
1 parent 52ae0e9 commit a538054

File tree

6 files changed

+67
-34
lines changed

6 files changed

+67
-34
lines changed

src/installer/managed/Microsoft.NET.HostModel/AppHost/HostWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ void RewriteAppHost(MemoryMappedFile mappedFile, MemoryMappedViewAccessor access
153153
MachObjectFile machObjectFile = MachObjectFile.Create(memoryMappedViewAccessor);
154154
appHostLength = machObjectFile.CreateAdHocSignature(memoryMappedViewAccessor, fileName);
155155
}
156-
else if (MachObjectFile.TryRemoveCodesign(memoryMappedViewAccessor, out long? length))
156+
else if (MachObjectFile.RemoveCodeSignatureIfPresent(memoryMappedViewAccessor, out long? length))
157157
{
158158
appHostLength = length.Value;
159159
}

src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
using System.Diagnostics;
77
using System.IO;
88
using System.IO.Compression;
9+
using System.IO.MemoryMappedFiles;
910
using System.Linq;
1011
using System.Reflection.PortableExecutable;
1112
using System.Runtime.InteropServices;
13+
using System.Text;
1214
using Microsoft.DotNet.CoreSetup;
1315
using Microsoft.NET.HostModel.AppHost;
16+
using Microsoft.NET.HostModel.MachO;
1417

1518
namespace Microsoft.NET.HostModel.Bundle
1619
{
@@ -92,7 +95,7 @@ private bool ShouldCompress(FileType type)
9295
/// startOffset: offset of the start 'file' within 'bundle'
9396
/// compressedSize: size of the compressed data, if entry was compressed, otherwise 0
9497
/// </returns>
95-
private (long startOffset, long compressedSize) AddToBundle(Stream bundle, FileStream file, FileType type)
98+
private (long startOffset, long compressedSize) AddToBundle(FileStream bundle, FileStream file, FileType type)
9699
{
97100
long startOffset = bundle.Position;
98101
if (ShouldCompress(type))
@@ -273,22 +276,20 @@ public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
273276

274277
BinaryUtils.CopyFile(hostSource, bundlePath);
275278

276-
if (_target.IsOSX && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && Codesign.IsAvailable)
277-
{
278-
Codesign.Run("--remove-signature", bundlePath);
279-
}
280-
281279
// Note: We're comparing file paths both on the OS we're running on as well as on the target OS for the app
282280
// We can't really make assumptions about the file systems (even on Linux there can be case insensitive file systems
283281
// and vice versa for Windows). So it's safer to do case sensitive comparison everywhere.
284282
var relativePathToSpec = new Dictionary<string, FileSpec>(StringComparer.Ordinal);
285283

286284
long headerOffset = 0;
287-
using (BinaryWriter writer = new BinaryWriter(File.OpenWrite(bundlePath)))
285+
using (FileStream bundle = File.Open(bundlePath, FileMode.Open, FileAccess.ReadWrite))
286+
using (BinaryWriter writer = new BinaryWriter(bundle, Encoding.Default, leaveOpen: true))
288287
{
289-
Stream bundle = writer.BaseStream;
288+
if (_target.IsOSX)
289+
{
290+
MachObjectFile.RemoveCodeSignatureIfPresent(bundle);
291+
}
290292
bundle.Position = bundle.Length;
291-
292293
foreach (var fileSpec in fileSpecs)
293294
{
294295
string relativePath = fileSpec.BundleRelativePath;

src/installer/managed/Microsoft.NET.HostModel/MachO/MachObjectFile.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public static bool IsMachOImage(string filePath)
143143
/// <param name="memoryMappedViewAccessor">The file to remove the signature from.</param>
144144
/// <param name="newLength">The new length of the file if the signature is remove and the method returns true</param>
145145
/// <returns>True if a signature was present and removed, false otherwise</returns>
146-
public static bool TryRemoveCodesign(MemoryMappedViewAccessor memoryMappedViewAccessor, out long? newLength)
146+
public static bool RemoveCodeSignatureIfPresent(MemoryMappedViewAccessor memoryMappedViewAccessor, out long? newLength)
147147
{
148148
newLength = null;
149149
if (!IsMachOImage(memoryMappedViewAccessor))
@@ -166,6 +166,25 @@ public static bool TryRemoveCodesign(MemoryMappedViewAccessor memoryMappedViewAc
166166
return true;
167167
}
168168

169+
/// <summary>
170+
/// Removes the code signature load command and signature, and resizes the file if necessary.
171+
/// </summary>
172+
public static void RemoveCodeSignatureIfPresent(FileStream bundle)
173+
{
174+
long? newLength;
175+
bool resized;
176+
// Windows doesn't allow a FileStream to be resized while the file is memory mapped, so we must dispose of the memory mapped file first.
177+
using (MemoryMappedFile mmap = MemoryMappedFile.CreateFromFile(bundle, null, 0, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
178+
using (MemoryMappedViewAccessor accessor = mmap.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite))
179+
{
180+
resized = RemoveCodeSignatureIfPresent(accessor, out newLength);
181+
}
182+
if (resized)
183+
{
184+
bundle.SetLength(newLength.Value);
185+
}
186+
}
187+
169188
/// <summary>
170189
/// Returns true if the two signed MachObjectFiles are equivalent.
171190
/// Since the entire file isn't store in the object, the code signature is required.

src/installer/tests/Microsoft.NET.HostModel.Tests/AppHost/CreateAppHost.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,12 +485,23 @@ private void ResourceWithUnknownLanguage()
485485
}
486486
}
487487

488-
private static readonly byte[] s_placeholderData = AppBinaryPathPlaceholderSearchValue.Concat(DotNetSearchPlaceholderValue).ToArray();
488+
private static readonly byte[] s_apphostPlaceholderData = AppBinaryPathPlaceholderSearchValue.Concat(DotNetSearchPlaceholderValue).ToArray();
489+
private static readonly byte[] s_singleFileApphostPlaceholderData = {
490+
// 8 bytes represent the bundle header-offset
491+
// Zero for non-bundle apphosts (default).
492+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
493+
// 32 bytes represent the bundle signature: SHA-256 for ".net core bundle"
494+
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
495+
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
496+
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
497+
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae
498+
};
499+
489500
/// <summary>
490501
/// Prepares a mock executable file with the AppHost placeholder embedded in it.
491502
/// This file will not run, but can be used to test HostWriter and signing process.
492503
/// </summary>
493-
public static string PrepareMockMachAppHostFile(string directory)
504+
public static string PrepareMockMachAppHostFile(string directory, bool singleFile = false)
494505
{
495506
string fileName = "MockAppHost.mach.o";
496507
string outputFilePath = Path.Combine(directory, fileName);
@@ -501,7 +512,7 @@ public static string PrepareMockMachAppHostFile(string directory)
501512
// Add the placeholder - it just needs to exist somewhere in the image
502513
// We'll put it at 4096 bytes into the file - this should be in the middle of the __TEXT segment
503514
managedSignFile.Position = 4096;
504-
managedSignFile.Write(s_placeholderData);
515+
managedSignFile.Write(singleFile ? s_singleFileApphostPlaceholderData : s_apphostPlaceholderData);
505516
}
506517
return outputFilePath;
507518
}

src/installer/tests/Microsoft.NET.HostModel.Tests/Bundle/BundlerConsistencyTests.cs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
using FluentAssertions;
1111
using Microsoft.DotNet.Cli.Build.Framework;
12+
using Microsoft.DotNet.CoreSetup;
1213
using Microsoft.DotNet.CoreSetup.Test;
14+
using Microsoft.NET.HostModel.AppHost.Tests;
15+
using Microsoft.NET.HostModel.MachO;
16+
using Microsoft.NET.HostModel.MachO.CodeSign.Tests;
1317
using Xunit;
1418

1519
namespace Microsoft.NET.HostModel.Bundle.Tests
@@ -24,8 +28,8 @@ public BundlerConsistencyTests(SharedTestState fixture)
2428
}
2529

2630
private static string BundlerHostName = Binaries.GetExeName(SharedTestState.AppName);
27-
private Bundler CreateBundlerInstance(BundleOptions bundleOptions = BundleOptions.None, Version version = null, bool macosCodesign = true)
28-
=> new Bundler(BundlerHostName, sharedTestState.App.GetUniqueSubdirectory("bundle"), bundleOptions, targetFrameworkVersion: version, macosCodesign: macosCodesign);
31+
private Bundler CreateBundlerInstance(BundleOptions bundleOptions = BundleOptions.None, Version version = null, bool macosCodesign = true, OSPlatform? targetOS = null)
32+
=> new Bundler(BundlerHostName, sharedTestState.App.GetUniqueSubdirectory("bundle"), bundleOptions, targetFrameworkVersion: version, macosCodesign: macosCodesign, targetOS: targetOS);
2933

3034
[Fact]
3135
public void EnableCompression_Before60_Fails()
@@ -313,34 +317,38 @@ public void AssemblyAlignment()
313317
[Theory]
314318
[InlineData(true)]
315319
[InlineData(false)]
316-
[PlatformSpecific(TestPlatforms.OSX)]
317-
public void Codesign(bool shouldCodesign)
320+
public void MacOSBundleIsCodeSigned(bool shouldCodesign)
318321
{
319322
TestApp app = sharedTestState.App;
320323
FileSpec[] fileSpecs = new FileSpec[]
321324
{
322-
new FileSpec(Binaries.AppHost.FilePath, BundlerHostName),
325+
new FileSpec(CreateAppHost.PrepareMockMachAppHostFile(app.Location, singleFile: true), BundlerHostName),
323326
new FileSpec(app.AppDll, Path.GetRelativePath(app.Location, app.AppDll)),
324327
new FileSpec(app.DepsJson, Path.GetRelativePath(app.Location, app.DepsJson)),
325328
new FileSpec(app.RuntimeConfigJson, Path.GetRelativePath(app.Location, app.RuntimeConfigJson)),
326329
};
327330

328-
Bundler bundler = CreateBundlerInstance(macosCodesign: shouldCodesign);
331+
Bundler bundler = CreateBundlerInstance(targetOS: OSPlatform.OSX, macosCodesign: shouldCodesign);
329332
string bundledApp = bundler.GenerateBundle(fileSpecs);
330333

331-
// Check if the file is signed
332-
CommandResult result = Command.Create("codesign", $"-v {bundledApp}")
333-
.CaptureStdErr()
334-
.CaptureStdOut()
335-
.Execute(expectedToFail: !shouldCodesign);
334+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
335+
{
336+
// SingleFile is still only signed on MacOS with codesign
337+
SigningTests.IsSigned(bundledApp).Should().BeFalse();
338+
return;
339+
}
336340

341+
// Check if the file is signed
342+
var result = Codesign.Run("-v", bundledApp);
337343
if (shouldCodesign)
338344
{
339-
result.Should().Pass();
345+
result.ExitCode.Should().Be(0);
340346
}
341347
else
342348
{
343-
result.Should().Fail();
349+
result.ExitCode.Should().NotBe(0);
350+
// Ensure we can sign it again
351+
Codesign.Run("-s -", bundledApp).ExitCode.Should().Be(0);
344352
}
345353
}
346354

src/installer/tests/Microsoft.NET.HostModel.Tests/MachObjectSigning/SigningTests.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,7 @@ internal static void RemoveSignature(string originalFilePath, string removedSign
228228
var destinationFileName = Path.GetFileName(removedSignaturePath);
229229
var appHostSignedLength = appHostLength + MachObjectFile.GetSignatureSizeEstimate((uint)appHostLength, destinationFileName);
230230

231-
using (MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationStream, null, appHostSignedLength, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true))
232-
using (MemoryMappedViewAccessor memoryMappedViewAccessor = memoryMappedFile.CreateViewAccessor(0, appHostSignedLength, MemoryMappedFileAccess.ReadWrite))
233-
{
234-
if (MachObjectFile.TryRemoveCodesign(memoryMappedViewAccessor, out long? newLength))
235-
appHostLength = newLength.Value;
236-
}
237-
appHostDestinationStream.SetLength(appHostLength);
231+
MachObjectFile.RemoveCodeSignatureIfPresent(appHostDestinationStream);
238232
}
239233
}
240234
}

0 commit comments

Comments
 (0)