diff --git a/Features/Codesigning/CodesigningMachOExtensions.cs b/Features/Codesigning/CodesigningMachOExtensions.cs index a974648..44bdd2c 100644 --- a/Features/Codesigning/CodesigningMachOExtensions.cs +++ b/Features/Codesigning/CodesigningMachOExtensions.cs @@ -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 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; } } \ No newline at end of file diff --git a/Features/Codesigning/CodesigningManager.cs b/Features/Codesigning/CodesigningManager.cs index 9e0ac43..d79ddbd 100644 --- a/Features/Codesigning/CodesigningManager.cs +++ b/Features/Codesigning/CodesigningManager.cs @@ -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 _allBinaries = []; - private List Binaries => _allBinaries[1..]; private IviMachOBinary MainBinary => _allBinaries[0]; + private List Binaries => _allBinaries[1..]; + private List MainExecutables => + _allBinaries.Where(binary => binary.IsMainExecutable(DirectoriesInfo)).ToList(); - private FileInfo? _savedMainEntitlements; + // + + private readonly ConcurrentDictionary _savedEntitlements = []; + private readonly List _mergedEntitlementFiles = []; internal void UpdateWithPackage(IviPackageInfo packageInfo) { @@ -73,87 +84,145 @@ internal class CodesigningManager(ILogger logger) : IDisposable }; } - internal async Task SaveMainBinaryEntitlementsAsync() + internal async Task 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 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 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 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 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 RemoveSignatureAsync(bool allBinaries) + internal async Task 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(); + } } \ No newline at end of file diff --git a/Features/Command/IviRootCommandProcessor.cs b/Features/Command/IviRootCommandProcessor.cs index 544f014..3f7beb9 100644 --- a/Features/Command/IviRootCommandProcessor.cs +++ b/Features/Command/IviRootCommandProcessor.cs @@ -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."); } } diff --git a/Features/Packaging/InfoPlistDictionaryExtensions.cs b/Features/Packaging/InfoPlistDictionaryExtensions.cs index 1ae0d15..0d42670 100644 --- a/Features/Packaging/InfoPlistDictionaryExtensions.cs +++ b/Features/Packaging/InfoPlistDictionaryExtensions.cs @@ -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()); } \ No newline at end of file diff --git a/Features/Packaging/PackageManager_Modifications.cs b/Features/Packaging/PackageManager_Modifications.cs index 00045ae..ab19303 100644 --- a/Features/Packaging/PackageManager_Modifications.cs +++ b/Features/Packaging/PackageManager_Modifications.cs @@ -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; diff --git a/ivinject.csproj b/ivinject.csproj index 6496e52..1cef1e6 100644 --- a/ivinject.csproj +++ b/ivinject.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable @@ -18,8 +18,8 @@ - - + +