From 2db47bca9a6459c5dcbb315f10f90e65a3eae061 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Sun, 28 Sep 2025 00:48:58 +0900 Subject: [PATCH] Add optional Authenticode signature validation for native DLLs Enhanced NativeBootstrap to support optional Authenticode signature validation and custom certificate validators when loading native libraries. Added 'NativeRequireSigned' configuration to app.config and resources, allowing signature enforcement to be toggled. Updated Program.cs to use the new option during initialization. --- .../WelsonJS.Launcher/NativeBootstrap.cs | 107 +++++++++++++++++- WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs | 12 +- .../Properties/Resources.Designer.cs | 9 ++ .../Properties/Resources.resx | 3 + .../WelsonJS.Launcher.csproj | 1 - WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 1 + 6 files changed, 123 insertions(+), 10 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs index 1b91330..609169b 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs @@ -4,10 +4,11 @@ // https://github.com/gnh1201/welsonjs // using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; namespace WelsonJS.Launcher { @@ -33,9 +34,18 @@ namespace WelsonJS.Launcher /// 1) %APPDATA%\{appDataSubdirectory}\{dllName} /// 2) Application base directory\{dllName} /// + /// Signatures: + /// - requireSigned = true : Only loads DLLs with a valid Authenticode chain. + /// - certValidator != null : Additional custom validation (e.g., pinning). + /// /// Must be called before any P/Invoke usage. /// - public static void Init(IEnumerable dllNames, string appDataSubdirectory, ICompatibleLogger logger) + public static void Init( + IEnumerable dllNames, + string appDataSubdirectory, + ICompatibleLogger logger, + bool requireSigned = false, + Func certValidator = null) { if (dllNames == null) throw new ArgumentNullException(nameof(dllNames)); if (logger == null) throw new ArgumentNullException(nameof(logger)); @@ -60,12 +70,12 @@ namespace WelsonJS.Launcher // 1) %APPDATA% subdirectory string candidate1 = Path.Combine(appDataPath, dllName); triedPaths.Add(candidate1); - if (TryLoad(candidate1, logger)) return; + if (TryLoad(candidate1, logger, requireSigned, certValidator)) return; // 2) Application base directory string candidate2 = Path.Combine(appBaseDirectory, dllName); triedPaths.Add(candidate2); - if (TryLoad(candidate2, logger)) return; + if (TryLoad(candidate2, logger, requireSigned, certValidator)) return; } string message = "Failed to load requested native libraries.\n" + @@ -74,7 +84,11 @@ namespace WelsonJS.Launcher throw new FileNotFoundException(message); } - private static bool TryLoad(string fullPath, ICompatibleLogger logger) + private static bool TryLoad( + string fullPath, + ICompatibleLogger logger, + bool requireSigned, + Func certValidator) { try { @@ -84,6 +98,14 @@ namespace WelsonJS.Launcher return false; } + // Optional signature validation + if (!ValidateSignatureIfRequired(fullPath, requireSigned, certValidator, logger)) + { + // If requireSigned=false we never reach here (it would return true). + logger.Warn($"Signature validation failed: {fullPath}"); + return false; + } + string directoryPath = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory; if (!TryRegisterSearchDirectory(directoryPath, logger)) { @@ -109,6 +131,81 @@ namespace WelsonJS.Launcher } } + /// + /// If requireSigned=false, returns true (no check). + /// If requireSigned=true, verifies Authenticode chain and optional custom validator. + /// + private static bool ValidateSignatureIfRequired( + string path, + bool requireSigned, + Func certValidator, + ICompatibleLogger logger) + { + if (!requireSigned) + { + // No signature requirement: allow loading regardless of signature. + return true; + } + + try + { + // Throws on unsigned files. + var baseCert = X509Certificate.CreateFromSignedFile(path); + if (baseCert == null) + { + logger.Warn("No certificate extracted from file."); + return false; + } + + var cert = new X509Certificate2(baseCert); + + var chain = new X509Chain + { + ChainPolicy = + { + RevocationMode = X509RevocationMode.Online, + RevocationFlag = X509RevocationFlag.ExcludeRoot, + VerificationFlags = X509VerificationFlags.NoFlag, + VerificationTime = DateTime.UtcNow + } + }; + + bool chainOk = chain.Build(cert); + if (!chainOk) + { + foreach (var status in chain.ChainStatus) + logger.Warn($"Cert chain status: {status.Status} - {status.StatusInformation?.Trim()}"); + return false; + } + + // Optional extra validation, e.g. thumbprint or subject pinning. + if (certValidator != null) + { + bool ok = false; + try { ok = certValidator(cert); } + catch (Exception ex) + { + logger.Warn($"Custom certificate validator threw: {ex.Message}"); + return false; + } + + if (!ok) + { + logger.Warn("Custom certificate validator rejected the certificate."); + return false; + } + } + + logger.Info($"Signature validated. Subject='{cert.Subject}', Thumbprint={cert.Thumbprint}"); + return true; + } + catch (Exception ex) + { + logger.Warn($"Signature check failed: {ex.Message}"); + return false; + } + } + private static bool TryRegisterSearchDirectory(string directoryPath, ICompatibleLogger logger) { try diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 485a3f3..682a4f4 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -15,7 +15,6 @@ namespace WelsonJS.Launcher { internal static class Program { - private const string _appDataSubDirectory = "WelsonJS"; private static readonly ICompatibleLogger _logger; public static Mutex _mutex; @@ -27,9 +26,14 @@ namespace WelsonJS.Launcher _logger = new TraceLogger(); // load native libraries - NativeBootstrap.Init(new string[] { - "ChakraCore.dll" - }, _appDataSubDirectory, _logger); + string appDataSubDirectory = "WelsonJS"; + bool requireSigned = GetAppConfig("NativeRequireSigned").Equals("true"); + NativeBootstrap.Init( + dllNames: new[] { "ChakraCore.dll" }, + appDataSubdirectory: appDataSubDirectory, + logger: _logger, + requireSigned: requireSigned + ); } [STAThread] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 35d0713..fdc2c3d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -286,6 +286,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// false과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string NativeRequireSigned { + get { + return ResourceManager.GetString("NativeRequireSigned", resourceCulture); + } + } + /// /// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index d198f04..dc3dc74 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -208,4 +208,7 @@ 5 + + false + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index a5b2e8e..0d5a5fc 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -92,7 +92,6 @@ - diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 8078a76..a26dab3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -21,6 +21,7 @@ +