Merge pull request #365 from gnh1201/dev

Update the integrity check (Catswords.Phantomizer)
This commit is contained in:
Namhyeon Go 2025-12-11 15:16:21 +09:00 committed by GitHub
commit 2cb422d075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 203 additions and 14 deletions

View File

@ -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();
}
}
}
}

View File

@ -1,4 +1,4 @@
# Phantomizer
# Catswords.Phantomizer
**Catswords.Phantomizer** is an HTTP-based dynamic-link library (DLL) loader designed for .NET applications.
It allows your application to fetch and load assemblies directly from your CDN (Azure Blob, S3, Cloudflare R2, etc.) at runtime, with optional GZip compression support.
@ -9,12 +9,13 @@ It allows your application to fetch and load assemblies directly from your CDN (
## 🚀 Features
* Load managed (`*.dll`) and native (`*.dll`) assemblies over HTTP **(HTTPS only)**
* Load managed (`*.dll`) and native (`*.dll`) assemblies over **HTTPS only**
* Optional `.dll.gz` decompression for faster network delivery
* CDN-friendly URL structure
* Easy bootstrap through a small embedded loader
* Loader is implemented using **pure .NET BCL only**, ensuring stable operation without external dependencies
* Loader is implemented using **pure .NET BCL only** without external dependencies (.NET Fx/Core fully supported)
* Built-in **code-signing verification** support to ensure assemblies are trusted and tamper-free
* An efficient integrity verification process based on an integrity manifest (NFT-grade immutability)
---
@ -67,6 +68,7 @@ private static void InitializeAssemblyLoader()
Type loaderType = phantomAsm.GetType("Catswords.Phantomizer.AssemblyLoader", true);
loaderType.GetProperty("BaseUrl")?.SetValue(null, GetAppConfig("AssemblyBaseUrl")); // Set the CDN base URL
//loaderType.GetProperty("IntegrityUrl")?.SetValue(null, GetAppConfig("IntegrityUrl")); // (Optional) Set the integrity URL
loaderType.GetProperty("LoaderNamespace")?.SetValue(null, typeof(Program).Namespace);
loaderType.GetProperty("AppName")?.SetValue(null, "WelsonJS"); // Application name
loaderType.GetMethod("Register")?.Invoke(null, null);
@ -99,6 +101,7 @@ using Catswords.Phantomizer;
static void Main(string[] args)
{
AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl"); // Configure CDN base URL
//AssemblyLoader.IntegrityUrl // (Optional) Set the integrity URL
AssemblyLoader.LoaderNamespace = typeof(Program).Namespace;
AssemblyLoader.AppName = "WelsonJS";
AssemblyLoader.Register();
@ -134,7 +137,47 @@ Once Phantomizer is initialized, your application will automatically fetch missi
---
## Download the pre-compiled file
## 🛡 Integrity Manifest (Integrity URL)
Phantomizer can verify assemblies before loading them by downloading an integrity manifest (XML).
You can host this integrity file anywhere — **preferably separate from your main CDN**, to prevent tampering and ensure independent verification of assembly integrity.
### 🔒 Why separate Integrity URL and main CDN?
Separating them prevents a compromised CDN bucket from serving malicious DLLs **and falsifying the integrity file**. Phantomizer can **trust the integrity manifest**, even if the main CDN is partially compromised.
### ✔ Recommended: Filebase (IPFS-pinning, NFT-grade immutability)
Filebase provides **immutable IPFS-based storage**, which is widely used in blockchain ecosystems — including **NFT metadata storage** — due to its strong guarantees of *content-addressing* and *tamper resistance*.
Once uploaded and pinned, the file cannot be silently modified without changing its IPFS hash (CID), making it ideal for hosting integrity manifests.
👉 **Recommended signup (with pinning support):** [Filebase](https://console.filebase.com/signup?ref=d44f5cc9cff7)
### ✔ Integrity Manifest Example (from `integrity.xml`)
```xml
<AssemblyIntegrity schemaVersion="1" generatedAt="2025-12-10T00:00:00Z">
<Hashes>
<Hash
value="5e274b47fc60c74159b4d1e21e70c0edf8e0936bdabc46b632525d09ca2fbae8"
algorithm="SHA256"
assemblyName="ChakraCore"
assemblyType="native"
version="1.13.0.0"
platform="x86"
compression="none"
fileName="ChakraCore.dll" />
<!-- ... more entries ... -->
</Hashes>
</AssemblyIntegrity>
```
---
## 📥 Download the pre-compiled file
* [Download Catswords.Phantomizer.dll.gz (catswords.blob.core.windows.net)](https://catswords.blob.core.windows.net/welsonjs/packages/managed/Catswords.Phantomizer/1.0.0.0/Catswords.Phantomizer.dll.gz)
---

View File

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

View File

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

View File

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

View File

@ -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"/>