welsonjs/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs
Namhyeon, Go 2db47bca9a 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.
2025-09-28 00:48:58 +09:00

244 lines
9.4 KiB
C#

// 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;
/// <summary>
/// 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.
/// </summary>
public static void Init(
IEnumerable<string> dllNames,
string appDataSubdirectory,
ICompatibleLogger logger,
bool requireSigned = false,
Func<X509Certificate2, bool> 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<string>();
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<X509Certificate2, bool> 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;
}
}
/// <summary>
/// If requireSigned=false, returns true (no check).
/// If requireSigned=true, verifies Authenticode chain and optional custom validator.
/// </summary>
private static bool ValidateSignatureIfRequired(
string path,
bool requireSigned,
Func<X509Certificate2, bool> 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;
}
}
}
}