// NativeBootstrap.cs // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors // https://github.com/gnh1201/welsonjs // using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; namespace WelsonJS.Launcher { public static class NativeBootstrap { // Win32 APIs [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr LoadLibrary(string lpFileName); [DllImport("kernel32", SetLastError = true)] private static extern bool SetDefaultDllDirectories(int DirectoryFlags); [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "AddDllDirectory")] private static extern IntPtr AddDllDirectory(string newDirectory); [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool SetDllDirectory(string lpPathName); private const int LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; /// /// Tries to load native libraries in the following order: /// 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, bool requireSigned = false, Func certValidator = null) { if (dllNames == null) throw new ArgumentNullException(nameof(dllNames)); if (logger == null) throw new ArgumentNullException(nameof(logger)); string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); string appDataPath = string.IsNullOrEmpty(appDataSubdirectory) ? appData : Path.Combine(appData, appDataSubdirectory); string asmLocation = Assembly.GetEntryAssembly()?.Location ?? Assembly.GetExecutingAssembly().Location ?? AppContext.BaseDirectory; string appBaseDirectory = Path.GetDirectoryName(asmLocation) ?? AppContext.BaseDirectory; var triedPaths = new List(); foreach (string dllName in dllNames) { if (string.IsNullOrWhiteSpace(dllName)) continue; // 1) %APPDATA% subdirectory string candidate1 = Path.Combine(appDataPath, dllName); triedPaths.Add(candidate1); if (TryLoad(candidate1, logger, requireSigned, certValidator)) return; // 2) Application base directory string candidate2 = Path.Combine(appBaseDirectory, dllName); triedPaths.Add(candidate2); if (TryLoad(candidate2, logger, requireSigned, certValidator)) return; } string message = "Failed to load requested native libraries.\n" + "Tried:\n " + string.Join("\n ", triedPaths); logger.Error(message); throw new FileNotFoundException(message); } private static bool TryLoad( string fullPath, ICompatibleLogger logger, bool requireSigned, Func certValidator) { try { if (!File.Exists(fullPath)) { logger.Info($"Not found: {fullPath}"); 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)) { logger.Warn($"Could not register search directory: {directoryPath}"); } logger.Info($"Loading: {fullPath}"); IntPtr handle = LoadLibrary(fullPath); if (handle == IntPtr.Zero) { int err = Marshal.GetLastWin32Error(); logger.Warn($"LoadLibrary failed for {fullPath} (Win32Error={err})."); return false; } logger.Info($"Successfully loaded: {fullPath}"); return true; } catch (Exception ex) { logger.Warn($"Exception while loading {fullPath}: {ex.Message}"); return false; } } /// /// 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 { bool ok = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); if (!ok) { int e = Marshal.GetLastWin32Error(); logger.Warn($"SetDefaultDllDirectories failed (Win32Error={e}), fallback to SetDllDirectory."); return SetDllDirectory(directoryPath); } IntPtr cookie = AddDllDirectory(directoryPath); if (cookie == IntPtr.Zero) { int e = Marshal.GetLastWin32Error(); logger.Warn($"AddDllDirectory failed (Win32Error={e}), fallback to SetDllDirectory."); return SetDllDirectory(directoryPath); } logger.Info($"Registered native DLL search directory: {directoryPath}"); return true; } catch (EntryPointNotFoundException) { logger.Warn("DefaultDllDirectories API not available. Using SetDllDirectory fallback."); return SetDllDirectory(directoryPath); } catch (Exception ex) { logger.Warn($"Register search directory failed for {directoryPath}: {ex.Message}"); return false; } } } }