diff --git a/src/OpenClaw.Connection/WindowsAuthenticodeVerifier.cs b/src/OpenClaw.Connection/WindowsAuthenticodeVerifier.cs index f2e190d3d..c6223964e 100644 --- a/src/OpenClaw.Connection/WindowsAuthenticodeVerifier.cs +++ b/src/OpenClaw.Connection/WindowsAuthenticodeVerifier.cs @@ -1,5 +1,12 @@ using System; +using System.Diagnostics; using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text.Json; +using System.Threading; using Microsoft.Security.Extensions; namespace OpenClaw.Connection; @@ -11,9 +18,83 @@ internal readonly record struct AuthenticodeTrustResult(bool IsTrusted, string? public static AuthenticodeTrustResult Rejected(string detail) => new(false, detail); } +/// +/// Minimal projection of a Windows AppX/MSIX package as reported by +/// Get-AppxPackage, used to corroborate genuine Microsoft WSL binaries that +/// ship without a per-file Authenticode/catalog signature (MSIX packages are +/// trusted at the package level, not per-file). +/// +internal readonly record struct AppxPackageInfo( + string? PackageFamilyName, + string? Publisher, + string? SignatureKind, + string? InstallLocation, + string? Version); + +internal readonly record struct WslRelayPathInspection( + bool IsSecure, + string? FileVersion, + string? Detail) +{ + public static WslRelayPathInspection Secure(string? fileVersion) => + new(true, fileVersion, null); + + public static WslRelayPathInspection Rejected(string detail) => + new(false, null, detail); +} + internal static class WindowsAuthenticodeVerifier { - public static AuthenticodeTrustResult VerifyMicrosoftSignedFile(string path) + // Modern WSL ships as an MSIX package. The trailing suffix is a hash derived + // from the publisher's signing certificate/public key, so an impostor package + // re-signed with a different certificate would get a different family name. + private const string WslPackageFamilyName = + "MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; + private const string TrustedInstallerSid = + "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"; + + public static AuthenticodeTrustResult VerifyMicrosoftSignedFile(string path) => + VerifyMicrosoftSignedFile( + path, + VerifyAuthenticodeSignature, + LookupWslAppxPackageViaPowerShell, + InspectWslRelayPath); + + /// + /// Test seam for the primary signature, WSL package, and protected-path + /// evidence used by the package fallback. + /// + internal static AuthenticodeTrustResult VerifyMicrosoftSignedFile( + string path, + Func verifyAuthenticode, + Func lookupWslPackage, + Func inspectRelayPath) + { + AuthenticodeTrustResult primary; + try + { + primary = verifyAuthenticode(path); + } + catch + { + return AuthenticodeTrustResult.Rejected( + "WSL relay Authenticode verification could not complete."); + } + + if (primary.IsTrusted) + return primary; + + // MSIX packages (modern WSL) don't carry a per-file Authenticode/catalog + // signature, so a failed classic check isn't conclusive for wslrelay.exe. + // Only consult the package-level fallback for the WSL binaries this + // codebase actually cares about; never widen trust for arbitrary files. + if (!string.Equals(Path.GetFileName(path), "wslrelay.exe", StringComparison.OrdinalIgnoreCase)) + return primary; + + return VerifyWslPackageFallback(path, primary, lookupWslPackage, inspectRelayPath); + } + + private static AuthenticodeTrustResult VerifyAuthenticodeSignature(string path) { try { @@ -45,6 +126,141 @@ public static AuthenticodeTrustResult VerifyMicrosoftSignedFile(string path) } } + private static AuthenticodeTrustResult VerifyWslPackageFallback( + string path, + AuthenticodeTrustResult primaryFailure, + Func lookupWslPackage, + Func inspectRelayPath) + { + AppxPackageInfo? package; + try + { + package = lookupWslPackage(); + } + catch + { + package = null; + } + + if (package is not { } info) + { + // No genuine WSL AppX package installed to corroborate; surface the + // original Authenticode diagnostic rather than a new, less useful one. + return primaryFailure; + } + + if (!string.Equals(info.PackageFamilyName, WslPackageFamilyName, StringComparison.Ordinal)) + { + // Doesn't match the well-known family name (a different/impostor + // package can't corroborate this binary); surface the original detail. + return primaryFailure; + } + + if (string.IsNullOrEmpty(info.SignatureKind) || + string.Equals(info.SignatureKind, "None", StringComparison.OrdinalIgnoreCase)) + { + return AuthenticodeTrustResult.Rejected( + "WSL package signature verification failed: the installed WSL package is unsigned."); + } + + if (info.Publisher is null || !HasMicrosoftPublisherIdentity(info.Publisher)) + { + return AuthenticodeTrustResult.Rejected( + "WSL package signature verification failed: the installed WSL package's publisher is not Microsoft Corporation."); + } + + if (string.IsNullOrWhiteSpace(info.InstallLocation) || + !Path.IsPathFullyQualified(info.InstallLocation)) + { + return AuthenticodeTrustResult.Rejected( + "WSL package provenance verification failed: package location data is unavailable."); + } + + string fullPath; + string installLocation; + try + { + fullPath = Path.GetFullPath(path); + installLocation = Path.GetFullPath(info.InstallLocation); + } + catch + { + return AuthenticodeTrustResult.Rejected( + "WSL package provenance verification failed: package location data is invalid."); + } + + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var windowsAppsRoot = Path.Combine(programFiles, "WindowsApps"); + if (IsPathWithin(installLocation, windowsAppsRoot) && + IsPathWithin(fullPath, installLocation)) + { + return ToTrustResult( + InspectProtectedRelayPath(fullPath, programFiles, inspectRelayPath)); + } + + var externalRelayPath = Path.Combine(programFiles, "WSL", "wslrelay.exe"); + if (!string.Equals(fullPath, externalRelayPath, StringComparison.OrdinalIgnoreCase)) + { + return AuthenticodeTrustResult.Rejected( + "WSL package provenance verification failed: the relay is not owned by the installed WSL package."); + } + + var inspection = InspectProtectedRelayPath( + fullPath, + programFiles, + inspectRelayPath); + if (!inspection.IsSecure) + return ToTrustResult(inspection); + + // The external WSL layout stamps wslrelay.exe with the package version. + // Exact equality binds the protected external bytes to the installed package. + if (!VersionsMatch(info.Version, inspection.FileVersion)) + { + return AuthenticodeTrustResult.Rejected( + "WSL package provenance verification failed: the external relay version does not match the installed WSL package."); + } + + return AuthenticodeTrustResult.Trusted(); + } + + private static WslRelayPathInspection InspectProtectedRelayPath( + string fullPath, + string trustedRoot, + Func inspectRelayPath) + { + try + { + return inspectRelayPath(fullPath, trustedRoot); + } + catch + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification failed: the relay path could not be inspected."); + } + } + + private static AuthenticodeTrustResult ToTrustResult(WslRelayPathInspection inspection) => + inspection.IsSecure + ? AuthenticodeTrustResult.Trusted() + : AuthenticodeTrustResult.Rejected( + inspection.Detail ?? + "WSL package provenance verification failed: the relay path is not protected."); + + private static bool VersionsMatch(string? packageVersion, string? fileVersion) => + Version.TryParse(packageVersion, out var parsedPackageVersion) && + Version.TryParse(fileVersion, out var parsedFileVersion) && + parsedPackageVersion.Equals(parsedFileVersion); + + private static bool IsPathWithin(string path, string root) + { + var relative = Path.GetRelativePath(root, path); + return !Path.IsPathRooted(relative) && + !string.Equals(relative, "..", StringComparison.Ordinal) && + !relative.StartsWith( + $"..{Path.DirectorySeparatorChar}", + StringComparison.Ordinal); + } + internal static bool HasMicrosoftPublisherIdentity(string subject) => subject.Split(',') .Select(part => part.Trim()) @@ -53,4 +269,249 @@ internal static bool HasMicrosoftPublisherIdentity(string subject) => part, "O=Microsoft Corporation", StringComparison.OrdinalIgnoreCase)); + + private static AppxPackageInfo? LookupWslAppxPackageViaPowerShell() + { + Process? process = null; + try + { + var psi = new ProcessStartInfo + { + FileName = ResolvePowerShellPath(), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.ArgumentList.Add("-NoProfile"); + psi.ArgumentList.Add("-NonInteractive"); + psi.ArgumentList.Add("-Command"); + psi.ArgumentList.Add( + "Get-AppxPackage -Name 'MicrosoftCorporationII.WindowsSubsystemForLinux' | " + + "Sort-Object Version -Descending | " + + "Select-Object -First 1 PackageFamilyName, Publisher, InstallLocation, " + + "@{Name='SignatureKind'; Expression={$_.SignatureKind.ToString()}}, " + + "@{Name='Version'; Expression={$_.Version.ToString()}} | " + + "ConvertTo-Json -Compress"); + + process = Process.Start(psi); + if (process is null) + return null; + + using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(8)); + var stdoutTask = process.StandardOutput.ReadToEndAsync(timeout.Token); + var stderrTask = process.StandardError.ReadToEndAsync(timeout.Token); + + if (!process.WaitForExit(8_000)) + { + try { process.Kill(entireProcessTree: true); } catch { } + return null; + } + + string output; + try + { + output = stdoutTask.GetAwaiter().GetResult(); + _ = stderrTask.GetAwaiter().GetResult(); + } + catch + { + return null; + } + + if (process.ExitCode != 0 || string.IsNullOrWhiteSpace(output)) + return null; + + return ParseAppxPackageJson(output); + } + catch + { + return null; + } + finally + { + process?.Dispose(); + } + } + + private static string ResolvePowerShellPath() + { + var systemRoot = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + if (string.IsNullOrWhiteSpace(systemRoot)) + systemRoot = Environment.GetEnvironmentVariable("SystemRoot"); + if (string.IsNullOrWhiteSpace(systemRoot)) + systemRoot = @"C:\Windows"; + return Path.Combine(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); + } + + private static AppxPackageInfo? ParseAppxPackageJson(string json) + { + try + { + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + if (root.ValueKind == JsonValueKind.Array) + { + if (root.GetArrayLength() == 0) + return null; + root = root[0]; + } + if (root.ValueKind != JsonValueKind.Object) + return null; + + var familyName = GetStringProperty(root, "PackageFamilyName"); + if (familyName is null) + return null; + + return new AppxPackageInfo( + familyName, + GetStringProperty(root, "Publisher"), + GetStringProperty(root, "SignatureKind"), + GetStringProperty(root, "InstallLocation"), + GetStringProperty(root, "Version")); + } + catch + { + return null; + } + } + + private static string? GetStringProperty(JsonElement element, string propertyName) => + element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String + ? value.GetString() + : null; + + private static WslRelayPathInspection InspectWslRelayPath( + string path, + string trustedRoot) + { + if (!OperatingSystem.IsWindows()) + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification is only available on Windows."); + } + + try + { + var fullPath = Path.GetFullPath(path); + var fullRoot = Path.GetFullPath(trustedRoot); + if (!File.Exists(fullPath) || !IsPathWithin(fullPath, fullRoot)) + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification failed: the relay path is unavailable or outside its trusted root."); + } + + for (var current = fullPath; ; current = Path.GetDirectoryName(current)!) + { + if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0) + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification failed: the relay path contains a reparse point."); + } + + FileSystemSecurity security = + string.Equals(current, fullPath, StringComparison.OrdinalIgnoreCase) + ? new FileInfo(current).GetAccessControl( + AccessControlSections.Owner | AccessControlSections.Access) + : new DirectoryInfo(current).GetAccessControl( + AccessControlSections.Owner | AccessControlSections.Access); + if (!HasProtectedOwnershipAndWriteAcl(security)) + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification failed: the relay path permits untrusted modification."); + } + + if (string.Equals(current, fullRoot, StringComparison.OrdinalIgnoreCase)) + break; + + if (string.IsNullOrWhiteSpace(Path.GetDirectoryName(current))) + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification failed: the relay path escaped its trusted root."); + } + } + + return WslRelayPathInspection.Secure( + FileVersionInfo.GetVersionInfo(fullPath).FileVersion); + } + catch + { + return WslRelayPathInspection.Rejected( + "WSL package provenance verification failed: the relay path could not be inspected."); + } + } + + [SupportedOSPlatform("windows")] + private static bool HasProtectedOwnershipAndWriteAcl(FileSystemSecurity security) + { + var owner = security.GetOwner(typeof(SecurityIdentifier)) as SecurityIdentifier; + var rules = security.GetAccessRules( + includeExplicit: true, + includeInherited: true, + targetType: typeof(SecurityIdentifier)) + .OfType(); + var descriptor = new RawSecurityDescriptor( + security.GetSecurityDescriptorBinaryForm(), + offset: 0); + var hasDiscretionaryAcl = + (descriptor.ControlFlags & ControlFlags.DiscretionaryAclPresent) != 0 && + descriptor.DiscretionaryAcl is not null; + return HasProtectedOwnershipAndWriteAcl( + owner, + rules, + hasDiscretionaryAcl); + } + + [SupportedOSPlatform("windows")] + internal static bool HasProtectedOwnershipAndWriteAcl( + SecurityIdentifier? owner, + IEnumerable rules, + bool hasDiscretionaryAcl) + { + if (!hasDiscretionaryAcl) + return false; + + var system = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null); + var administrators = new SecurityIdentifier( + WellKnownSidType.BuiltinAdministratorsSid, + null); + var trustedInstaller = new SecurityIdentifier(TrustedInstallerSid); + if (owner is null || + (!owner.Equals(system) && + !owner.Equals(administrators) && + !owner.Equals(trustedInstaller))) + { + return false; + } + + const FileSystemRights writeRights = + FileSystemRights.WriteData | + FileSystemRights.AppendData | + FileSystemRights.WriteExtendedAttributes | + FileSystemRights.WriteAttributes | + FileSystemRights.DeleteSubdirectoriesAndFiles | + FileSystemRights.Delete | + FileSystemRights.ChangePermissions | + FileSystemRights.TakeOwnership; + + foreach (var rule in rules) + { + if ((rule.PropagationFlags & PropagationFlags.InheritOnly) != 0 || + rule.AccessControlType != AccessControlType.Allow || + (rule.FileSystemRights & writeRights) == 0) + { + continue; + } + + if (rule.IdentityReference is not SecurityIdentifier sid || + (!sid.Equals(system) && + !sid.Equals(administrators) && + !sid.Equals(trustedInstaller))) + { + return false; + } + } + + return true; + } } diff --git a/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs b/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs index b5856a970..da0b5e7b2 100644 --- a/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs +++ b/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs @@ -1,4 +1,7 @@ using System.Net; +using System.Runtime.Versioning; +using System.Security.AccessControl; +using System.Security.Principal; using OpenClaw.Shared; using Xunit; @@ -53,6 +56,269 @@ public void VerifyMicrosoftSignedFile_RejectsUnsignedAssembly() Assert.Contains("Authenticode verification failed", result.Detail); } + private const string WslPackageFamilyName = + "MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; + private const string WslPackageInstallLocation = + @"C:\Program Files\WindowsApps\MicrosoftCorporationII.WindowsSubsystemForLinux_2.9.4.0_x64__8wekyb3d8bbwe"; + private const string ExternalWslRelayPath = @"C:\Program Files\WSL\wslrelay.exe"; + private const string PackageVersion = "2.9.4.0"; + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeFailsButProtectedExternalRelayMatchesPackage_IsTrusted() + { + var result = VerifyPackageFallback(ExternalWslRelayPath, ValidWslPackage()); + + Assert.True(result.IsTrusted, result.Detail); + } + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeFailsButPackageOwnsWindowsAppsRelay_IsTrusted() + { + var packageRelayPath = Path.Combine(WslPackageInstallLocation, "wslrelay.exe"); + + var result = VerifyPackageFallback( + packageRelayPath, + ValidWslPackage(), + WslRelayPathInspection.Secure("different-file-version")); + + Assert.True(result.IsTrusted, result.Detail); + } + + [Fact] + public void VerifyMicrosoftSignedFile_SiblingWindowsAppsRelay_IsRejectedWithoutPathInspection() + { + var inspected = false; + var siblingRelayPath = Path.Combine( + @"C:\Program Files\WindowsApps\SomeOther.Package_1.0.0.0_x64__deadbeefcafe", + "wslrelay.exe"); + + var result = WindowsAuthenticodeVerifier.VerifyMicrosoftSignedFile( + siblingRelayPath, + _ => AuthenticodeTrustResult.Rejected("Injected primary failure."), + () => ValidWslPackage(), + (_, _) => + { + inspected = true; + return WslRelayPathInspection.Secure(PackageVersion); + }); + + Assert.False(result.IsTrusted); + Assert.False(inspected); + Assert.Contains("not owned", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_ExternalRelayVersionMismatch_IsRejected() + { + var result = VerifyPackageFallback( + ExternalWslRelayPath, + ValidWslPackage(), + WslRelayPathInspection.Secure("2.9.3.0")); + + Assert.False(result.IsTrusted); + Assert.Contains("version does not match", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_ExternalRelayWithUnprotectedPath_IsRejected() + { + var result = VerifyPackageFallback( + ExternalWslRelayPath, + ValidWslPackage(), + WslRelayPathInspection.Rejected("Injected unprotected path.")); + + Assert.False(result.IsTrusted); + Assert.Contains("unprotected", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_RelativePackageLocation_IsRejectedWithoutPathInspection() + { + var inspected = false; + var package = ValidWslPackage() with { InstallLocation = @"relative\package" }; + + var result = WindowsAuthenticodeVerifier.VerifyMicrosoftSignedFile( + ExternalWslRelayPath, + _ => AuthenticodeTrustResult.Rejected("Injected primary failure."), + () => package, + (_, _) => + { + inspected = true; + return WslRelayPathInspection.Secure(PackageVersion); + }); + + Assert.False(result.IsTrusted); + Assert.False(inspected); + Assert.Contains("location data is unavailable", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeFailsAndNoWslPackageFound_IsRejectedWithOriginalDetail() + { + var result = VerifyPackageFallback(ExternalWslRelayPath, null); + + Assert.False(result.IsTrusted); + Assert.Contains("Injected primary failure", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeFailsAndWslPackageFamilyNameMismatches_IsRejected() + { + var result = VerifyPackageFallback( + ExternalWslRelayPath, + new AppxPackageInfo( + "SomeImpostor.WindowsSubsystemForLinux_deadbeefcafe", + "CN=Microsoft Windows, O=Microsoft Corporation, C=US", + "Developer", + WslPackageInstallLocation, + PackageVersion)); + + Assert.False(result.IsTrusted); + } + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeFailsAndWslPackageUnsigned_IsRejected() + { + var result = VerifyPackageFallback( + ExternalWslRelayPath, + new AppxPackageInfo( + WslPackageFamilyName, + "CN=Microsoft Windows, O=Microsoft Corporation, C=US", + "None", + WslPackageInstallLocation, + PackageVersion)); + + Assert.False(result.IsTrusted); + Assert.Contains("WSL package", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeFailsAndWslPackagePublisherNotMicrosoft_IsRejected() + { + var result = VerifyPackageFallback( + ExternalWslRelayPath, + new AppxPackageInfo( + WslPackageFamilyName, + "CN=Evil Corp, O=Evil Corp, C=US", + "Developer", + WslPackageInstallLocation, + PackageVersion)); + + Assert.False(result.IsTrusted); + Assert.Contains("WSL package", result.Detail, StringComparison.Ordinal); + } + + [Fact] + public void VerifyMicrosoftSignedFile_AuthenticodeSucceeds_NeverConsultsWslPackageFallback() + { + var fallbackInvoked = false; + + var result = WindowsAuthenticodeVerifier.VerifyMicrosoftSignedFile( + ExternalWslRelayPath, + _ => AuthenticodeTrustResult.Trusted(), + () => + { + fallbackInvoked = true; + throw new InvalidOperationException("Fallback should not be consulted."); + }, + (_, _) => throw new InvalidOperationException("Path should not be inspected.")); + + Assert.True(result.IsTrusted, result.Detail); + Assert.False(fallbackInvoked); + } + + [Fact] + public void VerifyMicrosoftSignedFile_NonRelayFailure_NeverConsultsWslPackageFallback() + { + var fallbackInvoked = false; + + var result = WindowsAuthenticodeVerifier.VerifyMicrosoftSignedFile( + @"C:\Program Files\WSL\wsl.exe", + _ => AuthenticodeTrustResult.Rejected("Injected primary failure."), + () => + { + fallbackInvoked = true; + return ValidWslPackage(); + }, + (_, _) => throw new InvalidOperationException("Path should not be inspected.")); + + Assert.False(result.IsTrusted); + Assert.Contains("Injected primary failure", result.Detail, StringComparison.Ordinal); + Assert.False(fallbackInvoked); + } + + private static AppxPackageInfo ValidWslPackage() => new( + WslPackageFamilyName, + "CN=Microsoft Windows, O=Microsoft Corporation, C=US", + "Developer", + WslPackageInstallLocation, + PackageVersion); + + private static AuthenticodeTrustResult VerifyPackageFallback( + string path, + AppxPackageInfo? package, + WslRelayPathInspection? pathInspection = null) => + WindowsAuthenticodeVerifier.VerifyMicrosoftSignedFile( + path, + _ => AuthenticodeTrustResult.Rejected("Injected primary failure."), + () => package, + (_, _) => pathInspection ?? WslRelayPathInspection.Secure(PackageVersion)); + + [Fact] + [SupportedOSPlatform("windows")] + public void HasProtectedOwnershipAndWriteAcl_AllowsReadOnlyUsersButRejectsUserWrite() + { + var system = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null); + var administrators = new SecurityIdentifier( + WellKnownSidType.BuiltinAdministratorsSid, + null); + var users = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + FileSystemAccessRule[] protectedRules = + [ + new(system, FileSystemRights.FullControl, AccessControlType.Allow), + new(administrators, FileSystemRights.Modify, AccessControlType.Allow), + new(users, FileSystemRights.ReadAndExecute, AccessControlType.Allow), + new( + new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null), + FileSystemRights.FullControl, + InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, + PropagationFlags.InheritOnly, + AccessControlType.Allow), + ]; + + Assert.True( + WindowsAuthenticodeVerifier.HasProtectedOwnershipAndWriteAcl( + system, + protectedRules, + hasDiscretionaryAcl: true)); + + var writableByUsers = protectedRules.Append( + new FileSystemAccessRule( + users, + FileSystemRights.WriteData, + AccessControlType.Allow)); + Assert.False( + WindowsAuthenticodeVerifier.HasProtectedOwnershipAndWriteAcl( + system, + writableByUsers, + hasDiscretionaryAcl: true)); + Assert.False( + WindowsAuthenticodeVerifier.HasProtectedOwnershipAndWriteAcl( + users, + protectedRules, + hasDiscretionaryAcl: true)); + Assert.False( + WindowsAuthenticodeVerifier.HasProtectedOwnershipAndWriteAcl( + system, + [], + hasDiscretionaryAcl: false)); + Assert.True( + WindowsAuthenticodeVerifier.HasProtectedOwnershipAndWriteAcl( + system, + [], + hasDiscretionaryAcl: true)); + } + [Theory] [InlineData("CN=Microsoft Windows, O=Microsoft Corporation, C=US", true)] [InlineData("CN=Microsoft Corporation Test Certificate, O=Example Corp, C=US", false)]