From 3bfe5e2c360b789b9641e70c08d8f9636fb73c88 Mon Sep 17 00:00:00 2001 From: Violet Hansen Date: Thu, 26 Dec 2024 18:37:34 +0200 Subject: [PATCH] Improved Merge operation After adding support for signed App Control policies to the AppControl Manager app, now adding support for Supplemental policy signers and Update policy signers to the merge functions. Improved the merge functions further to consider the signing scenario during deduplication of FileRules. Fixed a bug in Merge operation related to WHQLFilePublisher and WHQLPublisher level deduplication, their IDs were being considered instead of value. The Merge button now stays disabled while the merge operation is running. Added support for AllowAll/DenyAll FileRules in the merge functions. --- .../Pages/MergePolicies.xaml.cs | 15 ++- .../SiPolicy/Merger.Aggregate.cs | 120 +++++++++++++++++- AppControl Manager/SiPolicy/Merger.cs | 39 +++--- .../SiPolicyIntel/AllowRuleComparer.cs | 12 ++ .../SiPolicyIntel/DenyRuleComparer.cs | 12 ++ .../SiPolicyIntel/SignerCollection.cs | 2 + .../SupplementalPolicySignerRule.cs | 9 ++ .../SupplementalPolicySignerRuleComparer.cs | 103 +++++++++++++++ .../SiPolicyIntel/UpdatePolicySignerRule.cs | 9 ++ .../UpdatePolicySignerRuleComparer.cs | 103 +++++++++++++++ .../WHQLFilePublisherSignerRuleComparer.cs | 33 ++--- .../WHQLPublisherSignerRuleComparer.cs | 47 ++++--- 12 files changed, 443 insertions(+), 61 deletions(-) create mode 100644 AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRule.cs create mode 100644 AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRuleComparer.cs create mode 100644 AppControl Manager/SiPolicyIntel/UpdatePolicySignerRule.cs create mode 100644 AppControl Manager/SiPolicyIntel/UpdatePolicySignerRuleComparer.cs diff --git a/AppControl Manager/Pages/MergePolicies.xaml.cs b/AppControl Manager/Pages/MergePolicies.xaml.cs index 5d226020a..ab9b3536e 100644 --- a/AppControl Manager/Pages/MergePolicies.xaml.cs +++ b/AppControl Manager/Pages/MergePolicies.xaml.cs @@ -24,6 +24,12 @@ public MergePolicies() this.NavigationCacheMode = NavigationCacheMode.Enabled; } + + /// + /// Event handler for the main Merge button + /// + /// + /// private async void MergeButton_Click(object sender, RoutedEventArgs e) { @@ -53,6 +59,8 @@ private async void MergeButton_Click(object sender, RoutedEventArgs e) try { + MergeButton.IsEnabled = false; + PolicyMergerInfoBar.IsOpen = true; PolicyMergerInfoBar.Message = "Merging the policies"; @@ -113,6 +121,9 @@ await Task.Run(() => PolicyMergerInfoBar.IsClosable = true; MergeProgressRing.Visibility = Visibility.Collapsed; + + + MergeButton.IsEnabled = true; } } @@ -132,7 +143,7 @@ private void MainPolicyBrowseButton_Click(object sender, RoutedEventArgs e) mainPolicy = selectedFile; // Add the selected main XML policy file path to the flyout's TextBox - MainPolicy_Flyout_TextBox.Text += selectedFile; + MainPolicy_Flyout_TextBox.Text = selectedFile; } } @@ -147,7 +158,7 @@ private void MainPolicySettingsCard_Click(object sender, RoutedEventArgs e) mainPolicy = selectedFile; // Add the selected main XML policy file path to the flyout's TextBox - MainPolicy_Flyout_TextBox.Text += selectedFile; + MainPolicy_Flyout_TextBox.Text = selectedFile; } // Manually display the Flyout since user clicked/tapped on the Settings card and not the button itself diff --git a/AppControl Manager/SiPolicy/Merger.Aggregate.cs b/AppControl Manager/SiPolicy/Merger.Aggregate.cs index 091d3aa4c..96a2d2a27 100644 --- a/AppControl Manager/SiPolicy/Merger.Aggregate.cs +++ b/AppControl Manager/SiPolicy/Merger.Aggregate.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using AppControlManager.Logging; using AppControlManager.SiPolicyIntel; namespace AppControlManager.SiPolicy; @@ -146,6 +147,8 @@ internal static SignerCollection CollectSignerRules(List siPolicies) HashSet signerRules = new(new PublisherSignerRuleComparer()); HashSet whqlFilePublishers = new(new WHQLFilePublisherSignerRuleComparer()); HashSet wHQLPublishers = new(new WHQLPublisherSignerRuleComparer()); + HashSet updatePolicySignerRules = new(new UpdatePolicySignerRuleComparer()); + HashSet supplementalPolicySignerRules = new(new SupplementalPolicySignerRuleComparer()); // Loop over each policy input data foreach (SiPolicy siPolicy in siPolicies) @@ -156,8 +159,16 @@ internal static SignerCollection CollectSignerRules(List siPolicies) .ToDictionary(fileAttrib => fileAttrib.ID, fileAttrib => fileAttrib); // Get all of the elements from the policy - Dictionary signerDictionary = siPolicy.Signers - .ToDictionary(signer => signer.ID, signer => signer); + Dictionary signerDictionary = []; + + foreach (Signer signer in siPolicy.Signers) + { + if (!signerDictionary.TryAdd(signer.ID, signer)) + { + Logger.Write($"One of the XML files has more than 1 Signer with the same ID `{signer.ID}`"); + } + } + // ID of all of the CiSigners if they exist HashSet ciSignerSet = [.. siPolicy.CiSigners?.Select(ciSigner => ciSigner.SignerId) ?? []]; @@ -165,6 +176,20 @@ internal static SignerCollection CollectSignerRules(List siPolicies) // Dictionary to store all of the EKUs Dictionary ekuDictionary = siPolicy.EKUs?.ToDictionary(eku => eku.ID, eku => eku) ?? []; + // ID of all of the SupplementalPolicySigners if they exist + HashSet supplementalPolicySignersSet = [.. siPolicy.SupplementalPolicySigners?.Select(supplementalPolicySigner => supplementalPolicySigner.SignerId) ?? []]; + + // ID of all of the UpdatePolicySigners if they exist + HashSet updatePolicySignersSet = [.. siPolicy.UpdatePolicySigners?.Select(updatePolicySigner => updatePolicySigner.SignerId) ?? []]; + + + // Collecting UpdatePolicySigners and SupplementalPolicySigners separately + // Because they are not part of any SigningScenario and don't have Allowed/Denied signers + ProcessSupplementalPolicySigners(supplementalPolicySignersSet, signerDictionary, supplementalPolicySignerRules); + + ProcessUpdatePolicySigners(updatePolicySignersSet, signerDictionary, updatePolicySignerRules); + + // Step 2: Process SigningScenarios foreach (SigningScenario signingScenario in siPolicy.SigningScenarios) { @@ -234,7 +259,9 @@ internal static SignerCollection CollectSignerRules(List siPolicies) FilePublisherSigners = filePublisherSigners, SignerRules = signerRules, WHQLPublishers = wHQLPublishers, - WHQLFilePublishers = whqlFilePublishers + WHQLFilePublishers = whqlFilePublishers, + UpdatePolicySigners = updatePolicySignerRules, + SupplementalPolicySigners = supplementalPolicySignerRules }; } @@ -560,4 +587,91 @@ private static void AddSignerRule( } } + + + /// + /// Processes SupplementalPolicySigners + /// + /// + /// + /// + private static void ProcessSupplementalPolicySigners( + HashSet supplementalPolicySignerIDs, + Dictionary Signers, + HashSet supplementalPolicySignersSet) + { + + foreach (string ID in supplementalPolicySignerIDs) + { + if (Signers.TryGetValue(ID, out Signer? possibleSupplementalPolicySigner)) + { + + // Create random ID for the signer and its corresponding SupplementalPolicySigner element + string guid = GUIDGenerator.GenerateUniqueGUIDToUpper(); + string rand = $"ID_SIGNER_A_{guid}"; + + // Replace the Signer's ID + possibleSupplementalPolicySigner.ID = rand; + + + // Create a new SupplementalPolicySigner element with the new ID + SupplementalPolicySigner suppRule = new() + { + SignerId = rand + }; + + + _ = supplementalPolicySignersSet.Add(new SupplementalPolicySignerRule + { + SignerElement = possibleSupplementalPolicySigner, + SupplementalPolicySigner = suppRule + }); + } + } + } + + + + + /// + /// Processes UpdatePolicySigners + /// + /// + /// + /// + private static void ProcessUpdatePolicySigners( + HashSet updatePolicySignerIDs, + Dictionary Signers, + HashSet updatePolicySignersSet) + { + + foreach (string ID in updatePolicySignerIDs) + { + if (Signers.TryGetValue(ID, out Signer? possibleUpdatePolicySigner)) + { + + // Create random ID for the signer and its corresponding UpdatePolicySigner element + string guid = GUIDGenerator.GenerateUniqueGUIDToUpper(); + string rand = $"ID_SIGNER_A_{guid}"; + + // Replace the Signer's ID + possibleUpdatePolicySigner.ID = rand; + + // Create a new UpdatePolicySigner element with the new ID + UpdatePolicySigner uppRule = new() + { + SignerId = rand + }; + + _ = updatePolicySignersSet.Add(new UpdatePolicySignerRule + { + SignerElement = possibleUpdatePolicySigner, + UpdatePolicySigner = uppRule + }); + } + } + + + } + } diff --git a/AppControl Manager/SiPolicy/Merger.cs b/AppControl Manager/SiPolicy/Merger.cs index 88b85c316..aed914f9d 100644 --- a/AppControl Manager/SiPolicy/Merger.cs +++ b/AppControl Manager/SiPolicy/Merger.cs @@ -87,7 +87,9 @@ internal static void Merge(string mainXmlFilePath, HashSet otherXmlFileP IEnumerable signers = signerCollection.FilePublisherSigners.Select(x => x.SignerElement). Concat(signerCollection.WHQLFilePublishers.Select(x => x.SignerElement)). Concat(signerCollection.WHQLPublishers.Select(x => x.SignerElement)). - Concat(signerCollection.SignerRules.Select(x => x.SignerElement)); + Concat(signerCollection.SignerRules.Select(x => x.SignerElement)). + Concat(signerCollection.SupplementalPolicySigners.Select(x => x.SignerElement)). + Concat(signerCollection.UpdatePolicySigners.Select(x => x.SignerElement)); // Get all CiSigners IEnumerable ciSigners = signerCollection.WHQLPublishers.Where(x => x.SigningScenario is SSType.UserMode).Select(x => x.CiSignerElement!). @@ -130,6 +132,11 @@ internal static void Merge(string mainXmlFilePath, HashSet otherXmlFileP Concat(signerCollection.FilePublisherSigners.Where(x => x.SigningScenario is SSType.KernelMode && x.Auth is Authorization.Deny).Select(x => x.DeniedSignerElement))!; + IEnumerable supplementalPolicySignersCol = signerCollection.SupplementalPolicySigners.Where(x => x is not null).Select(x => x.SupplementalPolicySigner); + + IEnumerable updatePolicySignersCol = signerCollection.UpdatePolicySigners.Where(x => x is not null).Select(x => x.UpdatePolicySigner); + + // Construct the User Mode Signing Scenario SigningScenario UMCISigningScenario = new() { @@ -207,27 +214,27 @@ internal static void Merge(string mainXmlFilePath, HashSet otherXmlFileP // Create the final policy data, it will replace the content in the main XML file SiPolicy output = new() { - VersionEx = mainXML.VersionEx, // Main policy takes priority - PolicyTypeID = mainXML.PolicyTypeID, // Main policy takes priority - PlatformID = mainXML.PlatformID, // Main policy takes priority - PolicyID = mainXML.PolicyID, // Main policy takes priority - BasePolicyID = mainXML.BasePolicyID, // Main policy takes priority - Rules = mainXML.Rules, // Main policy takes priority + VersionEx = mainXML.VersionEx, // Main policy takes priority + PolicyTypeID = mainXML.PolicyTypeID, // Main policy takes priority + PlatformID = mainXML.PlatformID, // Main policy takes priority + PolicyID = mainXML.PolicyID, // Main policy takes priority + BasePolicyID = mainXML.BasePolicyID, // Main policy takes priority + Rules = mainXML.Rules, // Main policy takes priority EKUs = [.. ekusToUse], // Aggregated data FileRules = [.. fileRules], // Aggregated data - Signers = [.. signers], // Aggregated data + Signers = [.. signers], // Aggregated data SigningScenarios = [UMCISigningScenario, KMCISigningScenario], // Aggregated data - UpdatePolicySigners = mainXML.UpdatePolicySigners, // Main policy takes priority + UpdatePolicySigners = [.. updatePolicySignersCol], // Aggregated data CiSigners = [.. ciSigners], // Aggregated data HvciOptions = 2, // Set to the secure state HvciOptionsSpecified = true, // Set to the secure state - Settings = mainXML.Settings, // Main policy takes priority - Macros = mainXML.Macros, // Main policy takes priority - SupplementalPolicySigners = mainXML.SupplementalPolicySigners, // Main policy takes priority - AppSettings = mainXML.AppSettings, // Main policy takes priority - FriendlyName = mainXML.FriendlyName, // Main policy takes priority - PolicyType = mainXML.PolicyType, // Main policy takes priority - PolicyTypeSpecified = mainXML.PolicyTypeSpecified // Main policy takes priority + Settings = mainXML.Settings, // Main policy takes priority + Macros = mainXML.Macros, // Main policy takes priority + SupplementalPolicySigners = [.. supplementalPolicySignersCol], // Aggregated data + AppSettings = mainXML.AppSettings, // Main policy takes priority + FriendlyName = mainXML.FriendlyName, // Main policy takes priority + PolicyType = mainXML.PolicyType, // Main policy takes priority + PolicyTypeSpecified = mainXML.PolicyTypeSpecified // Main policy takes priority }; diff --git a/AppControl Manager/SiPolicyIntel/AllowRuleComparer.cs b/AppControl Manager/SiPolicyIntel/AllowRuleComparer.cs index 6359ec5e8..0fc5d5a79 100644 --- a/AppControl Manager/SiPolicyIntel/AllowRuleComparer.cs +++ b/AppControl Manager/SiPolicyIntel/AllowRuleComparer.cs @@ -13,6 +13,12 @@ public bool Equals(AllowRule? x, AllowRule? y) return false; } + // Check SSType + if (x.SigningScenario != y.SigningScenario) + { + return false; + } + Allow allowX = x.AllowElement; Allow allowY = y.AllowElement; @@ -38,6 +44,12 @@ public bool Equals(AllowRule? x, AllowRule? y) return true; } + // Rule special case: Check if FileName is "*" in both and are equal + if (string.Equals(allowX.FileName, "*", StringComparison.OrdinalIgnoreCase) && string.Equals(allowY.FileName, "*", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + // Rule 4: Check for MinimumFileVersion or MaximumFileVersion and the other properties bool hasMinX = !string.IsNullOrWhiteSpace(allowX.MinimumFileVersion); bool hasMaxX = !string.IsNullOrWhiteSpace(allowX.MaximumFileVersion); diff --git a/AppControl Manager/SiPolicyIntel/DenyRuleComparer.cs b/AppControl Manager/SiPolicyIntel/DenyRuleComparer.cs index 383f2c1df..64c04a8bc 100644 --- a/AppControl Manager/SiPolicyIntel/DenyRuleComparer.cs +++ b/AppControl Manager/SiPolicyIntel/DenyRuleComparer.cs @@ -13,6 +13,12 @@ public bool Equals(DenyRule? x, DenyRule? y) return false; } + // Check SSType + if (x.SigningScenario != y.SigningScenario) + { + return false; + } + Deny denyX = x.DenyElement; Deny denyY = y.DenyElement; @@ -38,6 +44,12 @@ public bool Equals(DenyRule? x, DenyRule? y) return true; } + // Rule special case: Check if FileName is "*" in both and are equal + if (string.Equals(denyX.FileName, "*", StringComparison.OrdinalIgnoreCase) && string.Equals(denyX.FileName, "*", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + // Rule 4: Check for MinimumFileVersion or MaximumFileVersion and the other properties bool hasMinX = !string.IsNullOrWhiteSpace(denyX.MinimumFileVersion); bool hasMaxX = !string.IsNullOrWhiteSpace(denyX.MaximumFileVersion); diff --git a/AppControl Manager/SiPolicyIntel/SignerCollection.cs b/AppControl Manager/SiPolicyIntel/SignerCollection.cs index 6e6380d93..8c5d90fb4 100644 --- a/AppControl Manager/SiPolicyIntel/SignerCollection.cs +++ b/AppControl Manager/SiPolicyIntel/SignerCollection.cs @@ -11,4 +11,6 @@ internal sealed class SignerCollection internal required HashSet SignerRules { get; set; } internal required HashSet WHQLPublishers { get; set; } internal required HashSet WHQLFilePublishers { get; set; } + internal required HashSet UpdatePolicySigners { get; set; } + internal required HashSet SupplementalPolicySigners { get; set; } } diff --git a/AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRule.cs b/AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRule.cs new file mode 100644 index 000000000..e87e62dba --- /dev/null +++ b/AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRule.cs @@ -0,0 +1,9 @@ +using AppControlManager.SiPolicy; + +namespace AppControlManager.SiPolicyIntel; + +internal sealed class SupplementalPolicySignerRule +{ + internal required Signer SignerElement { get; set; } + internal required SupplementalPolicySigner SupplementalPolicySigner { get; set; } +} diff --git a/AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRuleComparer.cs b/AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRuleComparer.cs new file mode 100644 index 000000000..e633570fe --- /dev/null +++ b/AppControl Manager/SiPolicyIntel/SupplementalPolicySignerRuleComparer.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using AppControlManager.SiPolicy; + +namespace AppControlManager.SiPolicyIntel; + +/// +/// Provides comparison logic for SupplementalPolicySignerRule objects based on their SignerElement. +/// This comparer supports two matching rules to determine equality. +/// +internal sealed class SupplementalPolicySignerRuleComparer : IEqualityComparer +{ + /// + /// Determines whether two SupplementalPolicySignerRule objects are equal based on their SignerElement. + /// + /// First SupplementalPolicySignerRule object. + /// Second SupplementalPolicySignerRule object. + /// True if the objects are considered equal, otherwise false. + public bool Equals(SupplementalPolicySignerRule? x, SupplementalPolicySignerRule? y) + { + // Null checks + if (x is null || y is null) + { + return false; + } + + // Extract signer elements for comparison + Signer signerX = x.SignerElement; + Signer signerY = y.SignerElement; + + // Rule 1: Check if Name, CertRoot.Value, and CertPublisher.Value are equal + if (IsSignerRule1Match(signerX, signerY)) + { + return true; + } + + // Rule 2: Check if Name and CertRoot.Value are equal + if (IsSignerRule2Match(signerX, signerY)) + { + return true; + } + + // If none of the rules match, return false + return false; + } + + /// + /// Generates a hash code for a SupplementalPolicySignerRule based on its SignerElement. + /// + /// The SupplementalPolicySignerRule object. + /// A hash code for the object. + public int GetHashCode(SupplementalPolicySignerRule obj) + { + ArgumentNullException.ThrowIfNull(obj); + + Signer signer = obj.SignerElement; + long hash = 17; // Initial hash value + const long modulus = 0x7FFFFFFF; // Maximum positive integer + + // Include Name in hash calculation if present + if (!string.IsNullOrWhiteSpace(signer.Name)) + { + hash = (hash * 31 + signer.Name.GetHashCode(StringComparison.OrdinalIgnoreCase)) % modulus; + } + + // Include CertRoot.Value in hash calculation if present + if (signer.CertRoot?.Value != null) + { + hash = (hash * 31 + CustomMethods.GetByteArrayHashCode(signer.CertRoot.Value)) % modulus; + } + + // Include CertPublisher.Value in hash calculation if present + if (!string.IsNullOrWhiteSpace(signer.CertPublisher?.Value)) + { + hash = (hash * 31 + signer.CertPublisher.Value.GetHashCode(StringComparison.OrdinalIgnoreCase)) % modulus; + } + + return (int)(hash & 0x7FFFFFFF); // Ensure non-negative hash + } + + /// + /// Rule 1: Name, CertRoot.Value, and CertPublisher.Value must match. + /// + private static bool IsSignerRule1Match(Signer signerX, Signer signerY) + { + return !string.IsNullOrWhiteSpace(signerX.Name) && + !string.IsNullOrWhiteSpace(signerY.Name) && + string.Equals(signerX.Name, signerY.Name, StringComparison.OrdinalIgnoreCase) && + BytesArrayComparer.AreByteArraysEqual(signerX.CertRoot?.Value, signerY.CertRoot?.Value) && + string.Equals(signerX.CertPublisher?.Value, signerY.CertPublisher?.Value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Rule 2: Name and CertRoot.Value must match. + /// + private static bool IsSignerRule2Match(Signer signerX, Signer signerY) + { + return !string.IsNullOrWhiteSpace(signerX.Name) && + !string.IsNullOrWhiteSpace(signerY.Name) && + string.Equals(signerX.Name, signerY.Name, StringComparison.OrdinalIgnoreCase) && + BytesArrayComparer.AreByteArraysEqual(signerX.CertRoot?.Value, signerY.CertRoot?.Value); + } +} diff --git a/AppControl Manager/SiPolicyIntel/UpdatePolicySignerRule.cs b/AppControl Manager/SiPolicyIntel/UpdatePolicySignerRule.cs new file mode 100644 index 000000000..76921419e --- /dev/null +++ b/AppControl Manager/SiPolicyIntel/UpdatePolicySignerRule.cs @@ -0,0 +1,9 @@ +using AppControlManager.SiPolicy; + +namespace AppControlManager.SiPolicyIntel; + +internal sealed class UpdatePolicySignerRule +{ + internal required Signer SignerElement { get; set; } + internal required UpdatePolicySigner UpdatePolicySigner { get; set; } +} diff --git a/AppControl Manager/SiPolicyIntel/UpdatePolicySignerRuleComparer.cs b/AppControl Manager/SiPolicyIntel/UpdatePolicySignerRuleComparer.cs new file mode 100644 index 000000000..84a79cc24 --- /dev/null +++ b/AppControl Manager/SiPolicyIntel/UpdatePolicySignerRuleComparer.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using AppControlManager.SiPolicy; + +namespace AppControlManager.SiPolicyIntel; + +/// +/// Provides comparison logic for UpdatePolicySignerRule objects based on their SignerElement. +/// This comparer supports two matching rules to determine equality. +/// +internal sealed class UpdatePolicySignerRuleComparer : IEqualityComparer +{ + /// + /// Determines whether two UpdatePolicySignerRule objects are equal based on their SignerElement. + /// + /// First UpdatePolicySignerRule object. + /// Second UpdatePolicySignerRule object. + /// True if the objects are considered equal, otherwise false. + public bool Equals(UpdatePolicySignerRule? x, UpdatePolicySignerRule? y) + { + // Null checks + if (x is null || y is null) + { + return false; + } + + // Extract signer elements for comparison + Signer signerX = x.SignerElement; + Signer signerY = y.SignerElement; + + // Rule 1: Check if Name, CertRoot.Value, and CertPublisher.Value are equal + if (IsSignerRule1Match(signerX, signerY)) + { + return true; + } + + // Rule 2: Check if Name and CertRoot.Value are equal + if (IsSignerRule2Match(signerX, signerY)) + { + return true; + } + + // If none of the rules match, return false + return false; + } + + /// + /// Generates a hash code for an UpdatePolicySignerRule based on its SignerElement. + /// + /// The UpdatePolicySignerRule object. + /// A hash code for the object. + public int GetHashCode(UpdatePolicySignerRule obj) + { + ArgumentNullException.ThrowIfNull(obj); + + Signer signer = obj.SignerElement; + long hash = 17; // Initial hash value + const long modulus = 0x7FFFFFFF; // Maximum positive integer + + // Include Name in hash calculation if present + if (!string.IsNullOrWhiteSpace(signer.Name)) + { + hash = (hash * 31 + signer.Name.GetHashCode(StringComparison.OrdinalIgnoreCase)) % modulus; + } + + // Include CertRoot.Value in hash calculation if present + if (signer.CertRoot?.Value != null) + { + hash = (hash * 31 + CustomMethods.GetByteArrayHashCode(signer.CertRoot.Value)) % modulus; + } + + // Include CertPublisher.Value in hash calculation if present + if (!string.IsNullOrWhiteSpace(signer.CertPublisher?.Value)) + { + hash = (hash * 31 + signer.CertPublisher.Value.GetHashCode(StringComparison.OrdinalIgnoreCase)) % modulus; + } + + return (int)(hash & 0x7FFFFFFF); // Ensure non-negative hash + } + + /// + /// Rule 1: Name, CertRoot.Value, and CertPublisher.Value must match. + /// + private static bool IsSignerRule1Match(Signer signerX, Signer signerY) + { + return !string.IsNullOrWhiteSpace(signerX.Name) && + !string.IsNullOrWhiteSpace(signerY.Name) && + string.Equals(signerX.Name, signerY.Name, StringComparison.OrdinalIgnoreCase) && + BytesArrayComparer.AreByteArraysEqual(signerX.CertRoot?.Value, signerY.CertRoot?.Value) && + string.Equals(signerX.CertPublisher?.Value, signerY.CertPublisher?.Value, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Rule 2: Name and CertRoot.Value must match. + /// + private static bool IsSignerRule2Match(Signer signerX, Signer signerY) + { + return !string.IsNullOrWhiteSpace(signerX.Name) && + !string.IsNullOrWhiteSpace(signerY.Name) && + string.Equals(signerX.Name, signerY.Name, StringComparison.OrdinalIgnoreCase) && + BytesArrayComparer.AreByteArraysEqual(signerX.CertRoot?.Value, signerY.CertRoot?.Value); + } +} diff --git a/AppControl Manager/SiPolicyIntel/WHQLFilePublisherSignerRuleComparer.cs b/AppControl Manager/SiPolicyIntel/WHQLFilePublisherSignerRuleComparer.cs index 40d30b5f4..05508797b 100644 --- a/AppControl Manager/SiPolicyIntel/WHQLFilePublisherSignerRuleComparer.cs +++ b/AppControl Manager/SiPolicyIntel/WHQLFilePublisherSignerRuleComparer.cs @@ -26,7 +26,7 @@ public bool Equals(WHQLFilePublisher? x, WHQLFilePublisher? y) // Rule 1: Check if Name, CertRoot.Value, and CertPublisher.Value are equal // And certEKUs match // For WHQLFilePublisher - if (IsSignerRule1Match(signerX, signerY) && DoCertEKUsMatch(signerX, signerY)) + if (IsSignerRule1Match(signerX, signerY) && DoEKUsMatch(x.Ekus, y.Ekus)) { // Merge the FileAttribElements of the ignored rule into the existing one MergeFileAttribElements(x, y); @@ -36,7 +36,7 @@ public bool Equals(WHQLFilePublisher? x, WHQLFilePublisher? y) // Rule 2: Check if Name and CertRoot.Value are equal // And certEKUs match // For WHQL but PCA/Root/Leaf certificate signer types - if (IsSignerRule2Match(signerX, signerY) && DoCertEKUsMatch(signerX, signerY)) + if (IsSignerRule2Match(signerX, signerY) && DoEKUsMatch(x.Ekus, y.Ekus)) { // Merge the FileAttribElements of the ignored rule into the existing one MergeFileAttribElements(x, y); @@ -88,12 +88,12 @@ public int GetHashCode(WHQLFilePublisher obj) hash = (hash * 31 + CustomMethods.GetByteArrayHashCode(signer.CertRoot.Value)) % modulus; } - // Rule 3: Include CertEKU IDs in the hash - foreach (CertEKU certEKU in signer.CertEKU ?? []) + // Rule 3: Include EKU Values + foreach (EKU eku in obj.Ekus) { - if (!string.IsNullOrWhiteSpace(certEKU.ID)) + if (eku.Value != null) { - hash = (hash * 31 + certEKU.ID.GetHashCode(StringComparison.OrdinalIgnoreCase)) % modulus; + hash = (hash * 31 + CustomMethods.GetByteArrayHashCode(eku.Value)) % modulus; } } @@ -133,20 +133,21 @@ private static bool IsSignerRule2Match(Signer signerX, Signer signerY) /// - /// Rule 3: CertEKU IDs must match + /// Rule 3: Compare EKU lists based on Value only (ignore IDs) /// - /// - /// - /// - private static bool DoCertEKUsMatch(Signer signerX, Signer signerY) + /// EKU list for first signer + /// EKU list for second signer + /// True if EKU values match + private static bool DoEKUsMatch(List ekusX, List ekusY) { - HashSet ekuIdsX = signerX.CertEKU?.Select(e => e.ID).Where(id => !string.IsNullOrWhiteSpace(id)).ToHashSet(StringComparer.OrdinalIgnoreCase) - ?? new HashSet(StringComparer.OrdinalIgnoreCase); - HashSet ekuIdsY = signerY.CertEKU?.Select(e => e.ID).Where(id => !string.IsNullOrWhiteSpace(id)).ToHashSet(StringComparer.OrdinalIgnoreCase) - ?? new HashSet(StringComparer.OrdinalIgnoreCase); + // Extract EKU values and ignore IDs + HashSet ekuValuesX = [.. ekusX.Where(e => e.Value != null).Select(e => CustomMethods.GetByteArrayHashCode(e.Value))]; + + HashSet ekuValuesY = [.. ekusY.Where(e => e.Value != null).Select(e => CustomMethods.GetByteArrayHashCode(e.Value))]; - return ekuIdsX.SetEquals(ekuIdsY); + // Compare sets of EKU values + return ekuValuesX.SetEquals(ekuValuesY); } diff --git a/AppControl Manager/SiPolicyIntel/WHQLPublisherSignerRuleComparer.cs b/AppControl Manager/SiPolicyIntel/WHQLPublisherSignerRuleComparer.cs index 8fa6fc381..8a19fffbc 100644 --- a/AppControl Manager/SiPolicyIntel/WHQLPublisherSignerRuleComparer.cs +++ b/AppControl Manager/SiPolicyIntel/WHQLPublisherSignerRuleComparer.cs @@ -31,7 +31,7 @@ public bool Equals(WHQLPublisher? x, WHQLPublisher? y) // Rule 1: Check if Name, CertRoot.Value, and CertPublisher.Value are equal // And CertEKUs match // For intermediate certificate type that uses full proper chain in signer - if (IsSignerRule1Match(signerX, signerY) && DoCertEKUsMatch(signerX, signerY)) + if (IsSignerRule1Match(signerX, signerY) && DoEKUsMatch(x.Ekus, y.Ekus)) { return true; } @@ -39,7 +39,7 @@ public bool Equals(WHQLPublisher? x, WHQLPublisher? y) // Rule 2: Check if Name and CertRoot.Value are equal // And CertEKUs match // For WHQL but PCA/Root/Leaf certificate signer types - if (IsSignerRule2Match(signerX, signerY) && DoCertEKUsMatch(signerX, signerY)) + if (IsSignerRule2Match(signerX, signerY) && DoEKUsMatch(x.Ekus, y.Ekus)) { return true; } @@ -49,6 +49,9 @@ public bool Equals(WHQLPublisher? x, WHQLPublisher? y) return false; } + /// + /// Generates a hash code for a WHQLPublisher object. + /// public int GetHashCode(WHQLPublisher obj) { ArgumentNullException.ThrowIfNull(obj); @@ -89,25 +92,23 @@ public int GetHashCode(WHQLPublisher obj) hash = (hash * 31 + CustomMethods.GetByteArrayHashCode(signer.CertRoot.Value)) % modulus; } - // Rule 3: Include CertEKU IDs in the hash - foreach (CertEKU certEKU in signer.CertEKU ?? []) + + // Rule 3: Include EKU Values + foreach (EKU eku in obj.Ekus) { - if (!string.IsNullOrWhiteSpace(certEKU.ID)) + if (eku.Value != null) { - hash = (hash * 31 + certEKU.ID.GetHashCode(StringComparison.OrdinalIgnoreCase)) % modulus; + hash = (hash * 31 + CustomMethods.GetByteArrayHashCode(eku.Value)) % modulus; } } - return (int)(hash & 0x7FFFFFFF); // Ensure non-negative hash value + // Ensure non-negative hash value + return (int)(hash & 0x7FFFFFFF); } - /// /// Rule 1: Name, CertRoot.Value, CertPublisher.Value must match /// - /// - /// - /// private static bool IsSignerRule1Match(Signer signerX, Signer signerY) { return !string.IsNullOrWhiteSpace(signerX.Name) && @@ -121,9 +122,6 @@ private static bool IsSignerRule1Match(Signer signerX, Signer signerY) /// /// Rule 2: Name and CertRoot.Value must match /// - /// - /// - /// private static bool IsSignerRule2Match(Signer signerX, Signer signerY) { return !string.IsNullOrWhiteSpace(signerX.Name) && @@ -134,20 +132,21 @@ private static bool IsSignerRule2Match(Signer signerX, Signer signerY) /// - /// Rule 3: CertEKU IDs must match + /// Rule 3: Compare EKU lists based on Value only (ignore IDs) /// - /// - /// - /// - private static bool DoCertEKUsMatch(Signer signerX, Signer signerY) + /// EKU list for first signer + /// EKU list for second signer + /// True if EKU values match + private static bool DoEKUsMatch(List ekusX, List ekusY) { - HashSet ekuIdsX = signerX.CertEKU?.Select(e => e.ID).Where(id => !string.IsNullOrWhiteSpace(id)).ToHashSet(StringComparer.OrdinalIgnoreCase) - ?? new HashSet(StringComparer.OrdinalIgnoreCase); - HashSet ekuIdsY = signerY.CertEKU?.Select(e => e.ID).Where(id => !string.IsNullOrWhiteSpace(id)).ToHashSet(StringComparer.OrdinalIgnoreCase) - ?? new HashSet(StringComparer.OrdinalIgnoreCase); + // Extract EKU values and ignore IDs + HashSet ekuValuesX = [.. ekusX.Where(e => e.Value != null).Select(e => CustomMethods.GetByteArrayHashCode(e.Value))]; + + HashSet ekuValuesY = [.. ekusY.Where(e => e.Value != null).Select(e => CustomMethods.GetByteArrayHashCode(e.Value))]; - return ekuIdsX.SetEquals(ekuIdsY); + // Compare sets of EKU values + return ekuValuesX.SetEquals(ekuValuesY); }