Skip to content

Commit c0b1c8f

Browse files
committed
Add test for codesigning bundles
1 parent 4f4002d commit c0b1c8f

File tree

3 files changed

+119
-72
lines changed

3 files changed

+119
-72
lines changed

test/Microsoft.NET.Build.Tests/AppHostTests.cs

Lines changed: 3 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
#nullable disable
55

6-
using System.Buffers.Binary;
76
using System.Diagnostics;
87
using System.Reflection.PortableExecutable;
98
using System.Text.RegularExpressions;
@@ -177,21 +176,12 @@ public void It_codesigns_an_app_targeting_osx(string targetFramework, string rid
177176
var appHostFullPath = Path.Combine(outputDirectory.FullName, testAssetName);
178177

179178
// Check that the apphost is signed
180-
HasMachOSignatureLoadCommand(new FileInfo(appHostFullPath)).Should().BeTrue();
181-
// When on a Mac, use the codesign tool to verify the signature as well
179+
MachOSignature.HasMachOSignatureLoadCommand(new FileInfo(appHostFullPath)).Should().BeTrue();
182180
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
183181
{
184-
var codesignPath = @"/usr/bin/codesign";
185-
new RunExeCommand(Log, codesignPath, ["-s", "-", appHostFullPath])
186-
.Execute()
182+
MachOSignature.HasValidMachOSignature(new FileInfo(appHostFullPath))
187183
.Should()
188-
.Fail()
189-
.And
190-
.HaveStdErrContaining($"{appHostFullPath}: is already signed");
191-
new RunExeCommand(Log, codesignPath, ["-v", appHostFullPath])
192-
.Execute()
193-
.Should()
194-
.Pass();
184+
.BeTrue($"The app host should have a valid Mach-O signature for {rid}.");
195185
}
196186
}
197187

@@ -487,62 +477,5 @@ private static bool IsPE32(string path)
487477
return reader.PEHeaders.PEHeader.Magic == PEMagic.PE32;
488478
}
489479
}
490-
491-
// Reads the Mach-O load commands and returns true if an LC_CODE_SIGNATURE command is found, otherwise returns false
492-
static bool HasMachOSignatureLoadCommand(FileInfo file)
493-
{
494-
/* Mach-O files have the following structure:
495-
* 32 byte header beginning with a magic number and info about the file and load commands
496-
* A series of load commands with the following structure:
497-
* - 4-byte command type
498-
* - 4-byte command size
499-
* - variable length command-specific data
500-
*/
501-
const uint LC_CODE_SIGNATURE = 0x1D;
502-
using (var stream = file.OpenRead())
503-
{
504-
// Read the MachO magic number to determine endianness
505-
Span<byte> eightByteBuffer = stackalloc byte[8];
506-
stream.ReadExactly(eightByteBuffer);
507-
// Determine if the magic number is in the same or opposite endianness as the runtime
508-
bool reverseEndinanness = BitConverter.ToUInt32(eightByteBuffer.Slice(0, 4)) switch
509-
{
510-
0xFEEDFACF => false,
511-
0xCFFAEDFE => true,
512-
_ => throw new InvalidOperationException("Not a 64-bit Mach-O file")
513-
};
514-
// 4-byte value at offset 16 is the number of load commands
515-
// 4-byte value at offset 20 is the size of the load commands
516-
stream.Position = 16;
517-
ReadUInts(stream, eightByteBuffer, out uint loadCommandsCount, out uint loadCommandsSize);
518-
// Mach-0 64 byte headers are 32 bytes long, and the first load command will be right after
519-
stream.Position = 32;
520-
bool hasSignature = false;
521-
for (int commandIndex = 0; commandIndex < loadCommandsCount; commandIndex++)
522-
{
523-
ReadUInts(stream, eightByteBuffer, out uint commandType, out uint commandSize);
524-
if (commandType == LC_CODE_SIGNATURE)
525-
{
526-
hasSignature = true;
527-
}
528-
stream.Position += commandSize - eightByteBuffer.Length;
529-
}
530-
Debug.Assert(stream.Position == loadCommandsSize + 32);
531-
return hasSignature;
532-
533-
void ReadUInts(Stream stream, Span<byte> buffer, out uint val1, out uint val2)
534-
{
535-
stream.ReadExactly(buffer);
536-
val1 = BitConverter.ToUInt32(buffer.Slice(0, 4));
537-
val2 = BitConverter.ToUInt32(buffer.Slice(4, 4));
538-
if (reverseEndinanness)
539-
{
540-
val1 = BinaryPrimitives.ReverseEndianness(val1);
541-
val2 = BinaryPrimitives.ReverseEndianness(val2);
542-
}
543-
}
544-
}
545-
}
546-
547480
}
548481
}

test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishASingleFileApp.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ private string GetNativeDll(string baseName)
8282
RuntimeInformation.RuntimeIdentifier.StartsWith("osx") ? "lib" + baseName + ".dylib" : "lib" + baseName + ".so";
8383
}
8484

85-
private DirectoryInfo GetPublishDirectory(PublishCommand publishCommand, string targetFramework = ToolsetInfo.CurrentTargetFramework)
85+
private DirectoryInfo GetPublishDirectory(PublishCommand publishCommand, string targetFramework = ToolsetInfo.CurrentTargetFramework, string runtimeIdentifier = null)
8686
{
8787
return publishCommand.GetOutputDirectory(targetFramework: targetFramework,
88-
runtimeIdentifier: RuntimeInformation.RuntimeIdentifier);
88+
runtimeIdentifier: runtimeIdentifier ?? RuntimeInformation.RuntimeIdentifier);
8989
}
9090

9191
[Fact]
@@ -1126,5 +1126,43 @@ static void VerifyPrepareForBundle(XDocument project)
11261126
new XAttribute("Condition", "'%(FilesToBundle.RelativePath)' == 'SingleFileTest.dll'"))));
11271127
}
11281128
}
1129+
1130+
[Theory]
1131+
[InlineData("osx-x64")]
1132+
[InlineData("osx-arm64")]
1133+
public void It_codesigns_an_app_targeting_osx(string rid)
1134+
{
1135+
var targetFramework = ToolsetInfo.CurrentTargetFramework;
1136+
var testProject = new TestProject()
1137+
{
1138+
Name = "SingleFileTest",
1139+
TargetFrameworks = targetFramework,
1140+
IsExe = true,
1141+
};
1142+
testProject.AdditionalProperties.Add("SelfContained", $"true");
1143+
1144+
var testAsset = _testAssetsManager.CreateTestProject(
1145+
testProject,
1146+
identifier: rid);
1147+
var publishCommand = new PublishCommand(testAsset);
1148+
1149+
publishCommand.Execute(PublishSingleFile, $"/p:RuntimeIdentifier={rid}", IncludeDefault)
1150+
.Should()
1151+
.Pass();
1152+
1153+
var publishDir = GetPublishDirectory(publishCommand, targetFramework, runtimeIdentifier: rid).FullName;
1154+
var singleFilePath = Path.Combine(publishDir, $"{testProject.Name}{Constants.ExeSuffix}");
1155+
1156+
MachOSignature.HasMachOSignatureLoadCommand(new FileInfo(singleFilePath))
1157+
.Should()
1158+
.BeTrue($"The app host should have a Mach-O signature load command for {rid}.");
1159+
1160+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
1161+
{
1162+
MachOSignature.HasValidMachOSignature(new FileInfo(singleFilePath), Log)
1163+
.Should()
1164+
.BeTrue($"The app host should have a valid Mach-O signature for {rid}.");
1165+
}
1166+
}
11291167
}
11301168
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.Versioning;
5+
using System.Buffers.Binary;
6+
using System.Diagnostics;
7+
8+
namespace Microsoft.NET.TestFramework.Utilities
9+
{
10+
public static class MachOSignature
11+
{
12+
[SupportedOSPlatform("osx")]
13+
public static bool HasValidMachOSignature(FileInfo file, ITestOutputHelper log)
14+
{
15+
var codesignPath = @"/usr/bin/codesign";
16+
return new RunExeCommand(log, codesignPath, "-v", file.FullName)
17+
.Execute().ExitCode == 0;
18+
}
19+
20+
// Reads the Mach-O load commands and returns true if an LC_CODE_SIGNATURE command is found, otherwise returns false
21+
public static bool HasMachOSignatureLoadCommand(FileInfo file)
22+
{
23+
/* Mach-O files have the following structure:
24+
* 32 byte header beginning with a magic number and info about the file and load commands
25+
* A series of load commands with the following structure:
26+
* - 4-byte command type
27+
* - 4-byte command size
28+
* - variable length command-specific data
29+
*/
30+
const uint LC_CODE_SIGNATURE = 0x1D;
31+
using (var stream = file.OpenRead())
32+
{
33+
// Read the MachO magic number to determine endianness
34+
Span<byte> eightByteBuffer = stackalloc byte[8];
35+
stream.ReadExactly(eightByteBuffer);
36+
// Determine if the magic number is in the same or opposite endianness as the runtime
37+
bool reverseEndinanness = BitConverter.ToUInt32(eightByteBuffer.Slice(0, 4)) switch
38+
{
39+
0xFEEDFACF => false,
40+
0xCFFAEDFE => true,
41+
_ => throw new InvalidOperationException("Not a 64-bit Mach-O file")
42+
};
43+
// 4-byte value at offset 16 is the number of load commands
44+
// 4-byte value at offset 20 is the size of the load commands
45+
stream.Position = 16;
46+
ReadUInts(stream, eightByteBuffer, out uint loadCommandsCount, out uint loadCommandsSize);
47+
// Mach-0 64 byte headers are 32 bytes long, and the first load command will be right after
48+
stream.Position = 32;
49+
bool hasSignature = false;
50+
for (int commandIndex = 0; commandIndex < loadCommandsCount; commandIndex++)
51+
{
52+
ReadUInts(stream, eightByteBuffer, out uint commandType, out uint commandSize);
53+
if (commandType == LC_CODE_SIGNATURE)
54+
{
55+
hasSignature = true;
56+
}
57+
stream.Position += commandSize - eightByteBuffer.Length;
58+
}
59+
Debug.Assert(stream.Position == loadCommandsSize + 32);
60+
return hasSignature;
61+
62+
void ReadUInts(Stream stream, Span<byte> buffer, out uint val1, out uint val2)
63+
{
64+
stream.ReadExactly(buffer);
65+
val1 = BitConverter.ToUInt32(buffer.Slice(0, 4));
66+
val2 = BitConverter.ToUInt32(buffer.Slice(4, 4));
67+
if (reverseEndinanness)
68+
{
69+
val1 = BinaryPrimitives.ReverseEndianness(val1);
70+
val2 = BinaryPrimitives.ReverseEndianness(val2);
71+
}
72+
}
73+
}
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)