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.
This commit is contained in:
Namhyeon Go 2025-09-28 00:48:58 +09:00
parent 87020d35ac
commit 2db47bca9a
6 changed files with 123 additions and 10 deletions

View File

@ -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.
/// </summary>
public static void Init(IEnumerable<string> dllNames, string appDataSubdirectory, ICompatibleLogger logger)
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));
@ -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<X509Certificate2, bool> 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
}
}
/// <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

View File

@ -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]

View File

@ -286,6 +286,15 @@ namespace WelsonJS.Launcher.Properties {
}
}
/// <summary>
/// false과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string NativeRequireSigned {
get {
return ResourceManager.GetString("NativeRequireSigned", resourceCulture);
}
}
/// <summary>
/// https://github.com/gnh1201/welsonjs과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>

View File

@ -208,4 +208,7 @@
<data name="ChromiumDevToolsTimeout" xml:space="preserve">
<value>5</value>
</data>
<data name="NativeRequireSigned" xml:space="preserve">
<value>false</value>
</data>
</root>

View File

@ -92,7 +92,6 @@
<Compile Include="JsNative.cs" />
<Compile Include="JsSerializer.cs" />
<Compile Include="NativeBootstrap.cs" />
<Compile Include="ResourceTools\ImageProxy.cs" />
<Compile Include="ResourceTools\IpQuery.cs" />
<Compile Include="ResourceTools\Settings.cs" />
<Compile Include="ResourceTools\Completion.cs" />

View File

@ -21,6 +21,7 @@
<add key="CitiApiKey" value=""/>
<add key="CitiApiPrefix" value="https://api.criminalip.io/v1/"/>
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
<add key="NativeRequireSigned" value="false"/>
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>