diff --git a/WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs b/WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs index abe4c2a..a22d509 100644 --- a/WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs +++ b/WelsonJS.Toolkit/Catswords.Phantomizer/AssemblyLoader.cs @@ -3,6 +3,9 @@ // SPDX-FileCopyrightText: Namhyeon Go , 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 _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(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: 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(); + } + } } } diff --git a/WelsonJS.Toolkit/Catswords.Phantomizer/README.md b/WelsonJS.Toolkit/Catswords.Phantomizer/README.md index 12f7527..bed858d 100644 --- a/WelsonJS.Toolkit/Catswords.Phantomizer/README.md +++ b/WelsonJS.Toolkit/Catswords.Phantomizer/README.md @@ -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 + + + + + + + +``` + +--- + +## πŸ“₯ 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) --- diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 965117a..5302310 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -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( diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index 095137a..ddfe573 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -69,6 +69,15 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZfκ³Ό(와) μœ μ‚¬ν•œ μ§€μ—­ν™”λœ λ¬Έμžμ—΄μ„ μ°ΎμŠ΅λ‹ˆλ‹€. + /// + internal static string AssemblyIntegrityUrl { + get { + return ResourceManager.GetString("AssemblyIntegrityUrl", resourceCulture); + } + } + /// /// κ³Ό(와) μœ μ‚¬ν•œ μ§€μ—­ν™”λœ λ¬Έμžμ—΄μ„ μ°ΎμŠ΅λ‹ˆλ‹€. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index 5bc52ea..7d96335 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -244,4 +244,7 @@ ..\Resources\Catswords.Phantomizer.dll.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + https://spare-yellow-cicada.myfilebase.com/ipfs/QmYL29Z7BRvE6dnL4HPKeNvJyNJrh2LnVhzo3k9bp3wBZf + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index ce96d91..c334ddd 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -30,6 +30,7 @@ +