mirror of
https://github.com/whoeevee/ivinject.git
synced 2026-01-08 23:25:03 +00:00
merging entitlements
This commit is contained in:
@@ -1,28 +1,31 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using ivinject.Common.Models;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
using static ivinject.Features.Packaging.Models.DirectoryNames;
|
||||
|
||||
namespace ivinject.Features.Codesigning;
|
||||
|
||||
internal static class CodesigningMachOExtensions
|
||||
{
|
||||
internal static bool IsMainExecutable(this IviMachOBinary binary, IviDirectoriesInfo directoriesInfo)
|
||||
{
|
||||
return !Path.GetRelativePath(
|
||||
directoriesInfo.BundleDirectory,
|
||||
binary.FullName
|
||||
).Contains(FrameworksDirectoryName);
|
||||
}
|
||||
|
||||
internal static async Task<bool> SignAsync(
|
||||
this IviMachOBinary binary,
|
||||
string identity,
|
||||
bool force,
|
||||
FileInfo? entitlements,
|
||||
bool preserveEntitlements = false
|
||||
FileInfo? entitlements = null
|
||||
)
|
||||
{
|
||||
var arguments = new StringBuilder($"-s {identity}");
|
||||
|
||||
if (entitlements is not null)
|
||||
arguments.Append($" --entitlements {entitlements.FullName}");
|
||||
else if (preserveEntitlements)
|
||||
arguments.Append(" --preserve-metadata=entitlements");
|
||||
|
||||
if (force)
|
||||
arguments.Append(" -f");
|
||||
|
||||
arguments.Append($" \"{binary.FullName}\"");
|
||||
|
||||
@@ -63,8 +66,10 @@ internal static class CodesigningMachOExtensions
|
||||
RedirectStandardError = true
|
||||
}
|
||||
);
|
||||
|
||||
var error = await process!.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
await process!.WaitForExitAsync();
|
||||
return process.ExitCode == 0;
|
||||
return process.ExitCode == 0 && error.Count(c => c.Equals('\n')) == 1;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Claunia.PropertyList;
|
||||
using ivinject.Common;
|
||||
using ivinject.Common.Models;
|
||||
using ivinject.Features.Codesigning.Models;
|
||||
using ivinject.Features.Command.Models;
|
||||
using ivinject.Features.Packaging;
|
||||
using ivinject.Features.Packaging.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static ivinject.Features.Packaging.Models.DirectoryNames;
|
||||
using static ivinject.Common.Models.BinaryHeaders;
|
||||
|
||||
namespace ivinject.Features.Codesigning;
|
||||
@@ -11,12 +14,20 @@ namespace ivinject.Features.Codesigning;
|
||||
internal class CodesigningManager(ILogger logger) : IDisposable
|
||||
{
|
||||
private IviPackageInfo _packageInfo = null!;
|
||||
private IviDirectoriesInfo DirectoriesInfo => _packageInfo.DirectoriesInfo;
|
||||
|
||||
//
|
||||
|
||||
private readonly List<IviMachOBinary> _allBinaries = [];
|
||||
private List<IviMachOBinary> Binaries => _allBinaries[1..];
|
||||
private IviMachOBinary MainBinary => _allBinaries[0];
|
||||
private List<IviMachOBinary> Binaries => _allBinaries[1..];
|
||||
private List<IviMachOBinary> MainExecutables =>
|
||||
_allBinaries.Where(binary => binary.IsMainExecutable(DirectoriesInfo)).ToList();
|
||||
|
||||
private FileInfo? _savedMainEntitlements;
|
||||
//
|
||||
|
||||
private readonly ConcurrentDictionary<IviMachOBinary, FileInfo> _savedEntitlements = [];
|
||||
private readonly List<FileInfo> _mergedEntitlementFiles = [];
|
||||
|
||||
internal void UpdateWithPackage(IviPackageInfo packageInfo)
|
||||
{
|
||||
@@ -73,87 +84,145 @@ internal class CodesigningManager(ILogger logger) : IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
internal async Task SaveMainBinaryEntitlementsAsync()
|
||||
internal async Task<bool> SaveMainExecutablesEntitlementsAsync()
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
if (await MainBinary.DumpEntitlementsAsync(tempFile))
|
||||
{
|
||||
logger.LogInformation("Saved {} entitlements", MainBinary.Name);
|
||||
_savedMainEntitlements = new FileInfo(tempFile);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning(
|
||||
"Unable to save {} entitlements. The binary is likely unsigned.",
|
||||
MainBinary.Name
|
||||
);
|
||||
}
|
||||
|
||||
internal async Task<bool> SignAsync(string identity, bool isAdHocSigning, FileInfo? entitlements)
|
||||
{
|
||||
var mainExecutablesCount = 0;
|
||||
|
||||
var signingResults = await Task.WhenAll(
|
||||
Binaries.Select(async binary =>
|
||||
var dumpingResults = await Task.WhenAll(
|
||||
MainExecutables.Select(async binary =>
|
||||
{
|
||||
var isMainExecutable = !Path.GetRelativePath(
|
||||
_packageInfo.DirectoriesInfo.BundleDirectory,
|
||||
binary.FullName
|
||||
).Contains(FrameworksDirectoryName);
|
||||
var tempFile = Path.GetTempFileName();
|
||||
var tempFileInfo = new FileInfo(tempFile);
|
||||
|
||||
if (isMainExecutable)
|
||||
mainExecutablesCount++;
|
||||
|
||||
return await binary.SignAsync(
|
||||
identity,
|
||||
isAdHocSigning,
|
||||
isMainExecutable
|
||||
? entitlements
|
||||
: null,
|
||||
isMainExecutable && entitlements is null
|
||||
);
|
||||
if (!await binary.DumpEntitlementsAsync(tempFile))
|
||||
{
|
||||
tempFileInfo.Delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
_savedEntitlements[binary] = tempFileInfo;
|
||||
return true;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return dumpingResults.All(result => result);
|
||||
}
|
||||
|
||||
private async Task<FileInfo> MergeEntitlementsAsync(
|
||||
string binaryName,
|
||||
FileInfo binaryEntitlements,
|
||||
FileInfo profileEntitlements
|
||||
)
|
||||
{
|
||||
var binaryEntitlementsDictionary = (NSDictionary)PropertyListParser.Parse(binaryEntitlements);
|
||||
var profileEntitlementsDictionary = (NSDictionary)PropertyListParser.Parse(profileEntitlements);
|
||||
|
||||
var profileTeamId = ((NSString)profileEntitlementsDictionary["com.apple.developer.team-identifier"]).Content;
|
||||
|
||||
var finalEntitlements = new NSDictionary();
|
||||
|
||||
foreach (var entitlementKey in binaryEntitlementsDictionary.Keys)
|
||||
{
|
||||
if (!profileEntitlementsDictionary.TryGetValue(entitlementKey, out var profileEntitlement))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Matching entitlement for {} ({}) was not found",
|
||||
entitlementKey,
|
||||
binaryName
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entitlementKey == "keychain-access-groups")
|
||||
{
|
||||
var binaryAccessGroups = (NSArray)binaryEntitlementsDictionary[entitlementKey];
|
||||
var finalAccessGroups = new NSArray();
|
||||
|
||||
foreach (var accessGroup in binaryAccessGroups)
|
||||
{
|
||||
var group = profileTeamId + ((NSString)accessGroup).Content[10..];
|
||||
finalAccessGroups.Add(new NSString(group));
|
||||
}
|
||||
|
||||
finalEntitlements[entitlementKey] = finalAccessGroups;
|
||||
logger.LogInformation("Mapped keychain access groups for {}: {}", binaryName, finalAccessGroups);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
finalEntitlements[entitlementKey] = profileEntitlement;
|
||||
}
|
||||
|
||||
var finalEntitlementsPath = Path.GetTempFileName();
|
||||
await finalEntitlements.SaveToFileAsync(finalEntitlementsPath);
|
||||
|
||||
var fileInfo = new FileInfo(finalEntitlementsPath);
|
||||
_mergedEntitlementFiles.Add(fileInfo);
|
||||
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
private async Task<bool> SignBinary(IviMachOBinary binary, IviSigningInfo signingInfo)
|
||||
{
|
||||
var identity = signingInfo.Identity;
|
||||
var entitlements = signingInfo.Entitlements;
|
||||
|
||||
if (!binary.IsMainExecutable(DirectoriesInfo))
|
||||
return await binary.SignAsync(identity);
|
||||
|
||||
if (!_savedEntitlements.TryGetValue(binary, out var signingEntitlements))
|
||||
return await binary.SignAsync(identity, entitlements);
|
||||
|
||||
if (!signingInfo.IsAdHocSigning)
|
||||
signingEntitlements = await MergeEntitlementsAsync(
|
||||
binary.Name,
|
||||
signingEntitlements,
|
||||
entitlements!
|
||||
);
|
||||
|
||||
return await binary.SignAsync(identity, signingEntitlements);
|
||||
}
|
||||
|
||||
internal async Task<bool> SignPackageAsync(IviSigningInfo signingInfo)
|
||||
{
|
||||
var signingResults = await Task.WhenAll(
|
||||
Binaries.Select(async binary => await SignBinary(binary, signingInfo))
|
||||
);
|
||||
|
||||
if (signingResults.Any(result => !result))
|
||||
return false;
|
||||
|
||||
if (!await MainBinary.SignAsync(identity, false, _savedMainEntitlements ?? entitlements))
|
||||
if (!await SignBinary(MainBinary, signingInfo))
|
||||
return false;
|
||||
|
||||
logger.LogInformation(
|
||||
"Signed {} binaries ({} main executables) with the specified identity",
|
||||
_allBinaries.Count,
|
||||
mainExecutablesCount + 1 // App main executable
|
||||
MainExecutables.Count
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal async Task<bool> RemoveSignatureAsync(bool allBinaries)
|
||||
internal async Task<bool> RemoveSignatureAsync()
|
||||
{
|
||||
if (allBinaries)
|
||||
{
|
||||
var removingResults = await Task.WhenAll(
|
||||
_allBinaries.Select(binary => binary.RemoveSignatureAsync())
|
||||
);
|
||||
|
||||
if (removingResults.Any(result => !result))
|
||||
return false;
|
||||
|
||||
logger.LogInformation("Signature removed from {} binaries", _allBinaries.Count);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!await MainBinary.RemoveSignatureAsync())
|
||||
var removingResults = await Task.WhenAll(
|
||||
_allBinaries.Select(binary => binary.RemoveSignatureAsync())
|
||||
);
|
||||
|
||||
if (removingResults.Any(result => !result))
|
||||
return false;
|
||||
|
||||
logger.LogInformation("Removed {} signature", MainBinary.Name);
|
||||
logger.LogInformation("Signature removed from {} binaries", _allBinaries.Count);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose() => _savedMainEntitlements?.Delete();
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var fileInfo in _savedEntitlements.Values)
|
||||
fileInfo.Delete();
|
||||
|
||||
foreach (var fileInfo in _mergedEntitlementFiles)
|
||||
fileInfo.Delete();
|
||||
}
|
||||
}
|
||||
@@ -76,21 +76,25 @@ internal class IviRootCommandProcessor
|
||||
|
||||
if (hasIdentity && !isAdHocSigning && !hasEntitlements)
|
||||
CriticalError("Entitlements are required for non ad hoc identity signing.");
|
||||
|
||||
if (isAdHocSigning && !hasEntitlements)
|
||||
await _codesigningManager.SaveMainBinaryEntitlementsAsync();
|
||||
|
||||
if (!await _codesigningManager.RemoveSignatureAsync(!hasIdentity || hasEntitlements))
|
||||
if (hasIdentity)
|
||||
{
|
||||
if (!await _codesigningManager.SaveMainExecutablesEntitlementsAsync())
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to save entitlements for one or more binaries. The package is likely unsigned, and all specified entitlements will be applied."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!await _codesigningManager.RemoveSignatureAsync())
|
||||
CriticalError("Unable to remove signature from one or more binaries.");
|
||||
|
||||
|
||||
await _injectionManager.InsertLoadCommandsAsync();
|
||||
|
||||
if (hasIdentity)
|
||||
{
|
||||
if (!await _codesigningManager.SignAsync(
|
||||
signingInfo!.Identity,
|
||||
isAdHocSigning,
|
||||
signingInfo.Entitlements))
|
||||
if (!await _codesigningManager.SignPackageAsync(signingInfo!))
|
||||
CriticalError("Unable to sign one or more binaries.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,6 @@ internal static class InfoPlistDictionaryExtensions
|
||||
internal static string BundleExecutable(this NSDictionary dictionary) =>
|
||||
((NSString)dictionary[CoreFoundationBundleExecutableKey]).Content;
|
||||
|
||||
internal static async Task SaveToFile(this NSDictionary dictionary, string filePath) =>
|
||||
internal static async Task SaveToFileAsync(this NSDictionary dictionary, string filePath) =>
|
||||
await File.WriteAllTextAsync(filePath, dictionary.ToXmlPropertyList());
|
||||
}
|
||||
@@ -77,7 +77,7 @@ internal partial class PackageManager
|
||||
var newBundleId = bundleId.Replace(packageBundleId, customBundleId);
|
||||
dictionary[CoreFoundationBundleIdentifierKey] = new NSString(newBundleId);
|
||||
|
||||
await dictionary.SaveToFile(file);
|
||||
await dictionary.SaveToFileAsync(file);
|
||||
replacedCount++;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ internal partial class PackageManager
|
||||
if (packagingInfo.EnableDocumentsSupport)
|
||||
EnableDocumentSupport();
|
||||
|
||||
await _infoDictionary.SaveToFile(_infoDictionaryFile.FullName);
|
||||
await _infoDictionary.SaveToFileAsync(_infoDictionaryFile.FullName);
|
||||
|
||||
if (packagingInfo.CustomBundleId is not { } customBundleId)
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0-rc.2.24473.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0-rc.2.24473.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0-preview.3.25171.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0-preview.3.25171.5" />
|
||||
<PackageReference Include="plist-cil" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user