mirror of
https://github.com/gnh1201/welsonjs.git
synced 2025-12-11 18:12:51 +00:00
Add hash-based assembly integrity check
Add hash-based assembly integrity check
This commit is contained in:
parent
dfb821d1d7
commit
0e15aa2b1f
|
|
@ -3,6 +3,9 @@
|
|||
// SPDX-FileCopyrightText: Namhyeon Go <gnh1201@catswords.re.kr>, 2025 Catswords OSS and WelsonJS Contributors
|
||||
// https://github.com/gnh1201/welsonjs
|
||||
//
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -33,6 +36,12 @@ namespace Catswords.Phantomizer
|
|||
public static string BaseUrl { get; set; } = null;
|
||||
public static string LoaderNamespace { get; set; } = typeof(AssemblyLoader).Namespace;
|
||||
public static string AppName { get; set; } = "Catswords";
|
||||
public static string IntegrityUrl { get; set; } = null;
|
||||
|
||||
// Hash whitelist (values only)
|
||||
private static HashSet<string> _integrityHashes = null;
|
||||
private static bool _integrityLoaded = false;
|
||||
private static readonly object IntegritySyncRoot = new object();
|
||||
|
||||
private static readonly object SyncRoot = new object();
|
||||
private static bool _registered;
|
||||
|
|
@ -167,16 +176,28 @@ namespace Catswords.Phantomizer
|
|||
if (_registered)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
try
|
||||
{
|
||||
Trace.TraceError("AssemblyLoader.Register() called but BaseUrl is not set.");
|
||||
throw new InvalidOperationException("AssemblyLoader.BaseUrl must be configured before Register().");
|
||||
}
|
||||
if (!_integrityLoaded)
|
||||
LoadIntegrityManifest();
|
||||
|
||||
if (!BaseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.IsNullOrWhiteSpace(BaseUrl))
|
||||
throw new InvalidOperationException("BaseUrl must be configured before Register().");
|
||||
|
||||
if (Uri.TryCreate(BaseUrl, UriKind.Absolute, out Uri uri))
|
||||
{
|
||||
if (uri.Scheme != Uri.UriSchemeHttps)
|
||||
throw new InvalidOperationException("BaseUrl must use HTTPS for security.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("BaseUrl is not a valid absolute URI.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.TraceError("AssemblyLoader.BaseUrl must use HTTPS for security.");
|
||||
throw new InvalidOperationException("AssemblyLoader.BaseUrl must use HTTPS.");
|
||||
Trace.TraceError("AssemblyLoader: failed to initialize: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
|
||||
|
|
@ -236,6 +257,7 @@ namespace Catswords.Phantomizer
|
|||
Trace.TraceInformation("Using cached native module: {0}", localPath);
|
||||
}
|
||||
|
||||
EnsureIntegrityOrThrow(localPath);
|
||||
EnsureSignedFileOrThrow(localPath, fileName);
|
||||
|
||||
IntPtr h = LoadLibrary(localPath);
|
||||
|
|
@ -324,6 +346,7 @@ namespace Catswords.Phantomizer
|
|||
return null;
|
||||
}
|
||||
|
||||
EnsureIntegrityOrThrow(dllPath);
|
||||
EnsureSignedFileOrThrow(dllPath, simpleName);
|
||||
return Assembly.LoadFrom(dllPath);
|
||||
}
|
||||
|
|
@ -447,14 +470,85 @@ namespace Catswords.Phantomizer
|
|||
|
||||
private static bool IsFrameworkAssembly(string name)
|
||||
{
|
||||
return name.StartsWith("System", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.StartsWith("Microsoft", StringComparison.OrdinalIgnoreCase) ||
|
||||
return name.StartsWith("System.", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase) ||
|
||||
name == "mscorlib" ||
|
||||
name == "netstandard" ||
|
||||
name == "WindowsBase" ||
|
||||
name == "PresentationCore" ||
|
||||
name == "PresentationFramework" ||
|
||||
name.StartsWith(LoaderNamespace, StringComparison.OrdinalIgnoreCase);
|
||||
name.StartsWith($"{LoaderNamespace}.", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void LoadIntegrityManifest()
|
||||
{
|
||||
lock (IntegritySyncRoot)
|
||||
{
|
||||
if (_integrityLoaded)
|
||||
return;
|
||||
|
||||
_integrityHashes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(IntegrityUrl))
|
||||
{
|
||||
_integrityLoaded = true;
|
||||
return; // integrity disabled
|
||||
}
|
||||
|
||||
XDocument doc;
|
||||
|
||||
try
|
||||
{
|
||||
if (Uri.TryCreate(IntegrityUrl, UriKind.Absolute, out Uri uri))
|
||||
{
|
||||
if (uri.Scheme != Uri.UriSchemeHttps)
|
||||
throw new InvalidOperationException("IntegrityUrl must use HTTPS for security.");
|
||||
|
||||
using (var res = Http.GetAsync(uri).GetAwaiter().GetResult())
|
||||
{
|
||||
res.EnsureSuccessStatusCode();
|
||||
using (var stream = res.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
|
||||
{
|
||||
doc = XDocument.Load(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("IntegrityUrl is not a valid absolute URI.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.TraceError("AssemblyIntegrity: failed to load manifest: {0}", ex.Message);
|
||||
throw new InvalidOperationException("Failed to load AssemblyIntegrity manifest.", ex);
|
||||
}
|
||||
|
||||
XElement hashes = doc.Root?.Element("Hashes");
|
||||
if (hashes == null)
|
||||
{
|
||||
Trace.TraceWarning("AssemblyIntegrity: <Hashes> not found. Integrity disabled.");
|
||||
_integrityLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var h in hashes.Elements("Hash"))
|
||||
{
|
||||
var algorithm = h.Attribute("algorithm")?.Value?.Trim();
|
||||
|
||||
if (!string.Equals(algorithm, "SHA256", StringComparison.OrdinalIgnoreCase))
|
||||
continue; // only SHA256 supported
|
||||
|
||||
string val = h.Attribute("value")?.Value?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(val))
|
||||
continue;
|
||||
|
||||
_integrityHashes.Add(val);
|
||||
}
|
||||
|
||||
_integrityLoaded = true;
|
||||
Trace.TraceInformation("AssemblyIntegrity: loaded {0} allowed hashes.", _integrityHashes.Count);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -484,6 +578,32 @@ namespace Catswords.Phantomizer
|
|||
throw new InvalidOperationException("Invalid signature: " + logicalName);
|
||||
}
|
||||
|
||||
private static void EnsureIntegrityOrThrow(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(IntegrityUrl))
|
||||
return; // disabled
|
||||
|
||||
if (_integrityHashes == null || _integrityHashes.Count == 0)
|
||||
{
|
||||
Trace.TraceWarning("AssemblyIntegrity: no hashes loaded → skipping check.");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] bytes = File.ReadAllBytes(path);
|
||||
|
||||
// Compute hashes
|
||||
string sha256 = ComputeHashHex(bytes, SHA256.Create());
|
||||
|
||||
// Check match
|
||||
if (_integrityHashes.Contains(sha256))
|
||||
{
|
||||
Trace.TraceInformation("AssemblyIntegrity: hash OK for {0}", Path.GetFileName(path));
|
||||
return;
|
||||
}
|
||||
|
||||
Trace.TraceError("AssemblyIntegrity: hash mismatch! SHA256={0}", sha256);
|
||||
throw new InvalidOperationException("AssemblyIntegrity check failed for: " + path);
|
||||
}
|
||||
|
||||
private static FileSignatureStatus VerifySignature(string file)
|
||||
{
|
||||
|
|
@ -502,5 +622,17 @@ namespace Catswords.Phantomizer
|
|||
|
||||
return FileSignatureStatus.Invalid;
|
||||
}
|
||||
|
||||
private static string ComputeHashHex(byte[] data, HashAlgorithm algorithm)
|
||||
{
|
||||
using (algorithm)
|
||||
{
|
||||
var hash = algorithm.ComputeHash(data);
|
||||
var sb = new StringBuilder(hash.Length * 2);
|
||||
foreach (var b in hash)
|
||||
sb.Append(b.ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ namespace WelsonJS.Launcher
|
|||
loaderType.GetProperty("BaseUrl")?.SetValue(null, GetAppConfig("AssemblyBaseUrl"));
|
||||
loaderType.GetProperty("LoaderNamespace")?.SetValue(null, typeof(Program).Namespace);
|
||||
loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS");
|
||||
//loaderType.GetProperty("IntegrityUrl")?.SetValue(null, GetAppConfig("AssemblyIntegrityUrl")); // In the future, we may use this to verify integrity.
|
||||
loaderType.GetMethod("Register")?.Invoke(null, null);
|
||||
|
||||
var loadNativeModulesMethod = loaderType.GetMethod(
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@ namespace WelsonJS.Launcher.Properties {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZf과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string AssemblyIntegrityUrl {
|
||||
get {
|
||||
return ResourceManager.GetString("AssemblyIntegrityUrl", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -244,4 +244,7 @@
|
|||
<data name="Phantomizer" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>..\Resources\Catswords.Phantomizer.dll.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="AssemblyIntegrityUrl" xml:space="preserve">
|
||||
<value>https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZf</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<add key="TelemetryBaseUrl" value="https://us.i.posthog.com"/>
|
||||
<add key="TelemetryEnabled" value="true"/>
|
||||
<add key="AssemblyBaseUrl" value="https://catswords.blob.core.windows.net/welsonjs/packages"/>
|
||||
<add key="AssemblyIntegrityUrl" value="https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZf"/>
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user