security: harden ToolHub update trust model

This commit is contained in:
7015725
2026-05-07 23:46:46 +08:00
parent fdd896aca5
commit 726ed4465a
5 changed files with 81 additions and 13 deletions

View File

@@ -4,7 +4,11 @@
var GIT_ROOT = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/"; var GIT_ROOT = "https://git.xin-blog.com/linshenjianlu/ShortX_ToolHub/raw/branch/main/";
var GIT_BASE = GIT_ROOT + "code/"; var GIT_BASE = GIT_ROOT + "code/";
var TRUSTED_PUBLIC_KEY_B64 = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApiyhtMDJce7dVCxH1/oDu8kbiECYoT5XXmXvR/XNYuJ/5FuL83SbpCQ3QmUnqkbfNyOFqnxac/qlbXJtx6eeSotLP1HmrKI0LGymgxG6b1FfGHBfIKNZfBLIvzVDQob+HJfshlsS1JRlW5Jhm25TMh8dJCQQQZWW/ZItbtOvPYbLwG8cnqEdX8gqyB304+r2l35GPTfxZIGEK/9PcE3AMuqwTolMJsBHtG61hmMdz3dzTTEZQoOcciGWuwr2ZW8XkF6f5SgWkC29ZxZqAxceK4FJ8BsYirpFQxVKyZ6eiYlpNiYz+pHLP2U7JTO6ImmT1rlYSS6xw2tlWf0xq72nuOPC+VzEivuEhnC4y9WBSvauRa/ViIDgQ3yXl2MajuAvGSVWRfZ5Gz5Up8PQD7vxmHT2r0fA4xq4GIvUvGCqOG/d1FRrlVyEuNhCZ7KgpEKPno7fLnC6/ftnYcN5ZNOSWwjWH/e4fBxM5s6RRIYzIY2N0f/fqsRH42lWAhX5stujAgMBAAE="; var TRUSTED_PUBLIC_KEYS = {
"toolhub-targets-2026-rsa3072": "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEApiyhtMDJce7dVCxH1/oDu8kbiECYoT5XXmXvR/XNYuJ/5FuL83SbpCQ3QmUnqkbfNyOFqnxac/qlbXJtx6eeSotLP1HmrKI0LGymgxG6b1FfGHBfIKNZfBLIvzVDQob+HJfshlsS1JRlW5Jhm25TMh8dJCQQQZWW/ZItbtOvPYbLwG8cnqEdX8gqyB304+r2l35GPTfxZIGEK/9PcE3AMuqwTolMJsBHtG61hmMdz3dzTTEZQoOcciGWuwr2ZW8XkF6f5SgWkC29ZxZqAxceK4FJ8BsYirpFQxVKyZ6eiYlpNiYz+pHLP2U7JTO6ImmT1rlYSS6xw2tlWf0xq72nuOPC+VzEivuEhnC4y9WBSvauRa/ViIDgQ3yXl2MajuAvGSVWRfZ5Gz5Up8PQD7vxmHT2r0fA4xq4GIvUvGCqOG/d1FRrlVyEuNhCZ7KgpEKPno7fLnC6/ftnYcN5ZNOSWwjWH/e4fBxM5s6RRIYzIY2N0f/fqsRH42lWAhX5stujAgMBAAE="
};
var DEFAULT_TRUSTED_KEY_ID = "toolhub-targets-2026-rsa3072";
var MIN_TRUSTED_MANIFEST_VERSION = 20260507152251;
var __dirChecked = false; var __dirChecked = false;
var __trustedManifest = null; var __trustedManifest = null;
var __securityStatus = { ok: false, msg: "安全清单尚未校验" }; var __securityStatus = { ok: false, msg: "安全清单尚未校验" };
@@ -194,9 +198,17 @@ function base64Decode(s) {
return android.util.Base64.decode(String(s).replace(/\s+/g, ""), android.util.Base64.DEFAULT); return android.util.Base64.decode(String(s).replace(/\s+/g, ""), android.util.Base64.DEFAULT);
} }
function verifyManifestSignature(manifestText, sigText) { function getTrustedPublicKeyB64(keyId) {
var kid = keyId ? String(keyId) : DEFAULT_TRUSTED_KEY_ID;
if (TRUSTED_PUBLIC_KEYS.hasOwnProperty(kid)) return TRUSTED_PUBLIC_KEYS[kid];
return null;
}
function verifyManifestSignature(manifestText, sigText, keyId) {
try { try {
var pubBytes = base64Decode(TRUSTED_PUBLIC_KEY_B64); var pubB64 = getTrustedPublicKeyB64(keyId);
if (!pubB64) throw "unknown manifest keyId: " + String(keyId);
var pubBytes = base64Decode(pubB64);
var sigBytes = base64Decode(sigText); var sigBytes = base64Decode(sigText);
var spec = new java.security.spec.X509EncodedKeySpec(pubBytes); var spec = new java.security.spec.X509EncodedKeySpec(pubBytes);
var pubKey = java.security.KeyFactory.getInstance("RSA").generatePublic(spec); var pubKey = java.security.KeyFactory.getInstance("RSA").generatePublic(spec);
@@ -215,16 +227,19 @@ function fetchTrustedManifest() {
ensureCodeDir(); ensureCodeDir();
var manifestText = downloadText(GIT_ROOT + "manifest.json"); var manifestText = downloadText(GIT_ROOT + "manifest.json");
var sigText = downloadText(GIT_ROOT + "manifest.sig"); var sigText = downloadText(GIT_ROOT + "manifest.sig");
if (!verifyManifestSignature(manifestText, sigText)) throw "manifest signature invalid";
var manifest = JSON.parse(String(manifestText)); var manifest = JSON.parse(String(manifestText));
if (!manifest || !manifest.files) throw "manifest files missing"; if (!manifest || !manifest.files) throw "manifest files missing";
if (String(manifest.alg || "") !== "SHA256withRSA") throw "unsupported manifest alg: " + String(manifest.alg); if (String(manifest.alg || "") !== "SHA256withRSA") throw "unsupported manifest alg: " + String(manifest.alg);
var keyId = String(manifest.keyId || DEFAULT_TRUSTED_KEY_ID);
if (!getTrustedPublicKeyB64(keyId)) throw "untrusted manifest keyId: " + keyId;
if (!verifyManifestSignature(manifestText, sigText, keyId)) throw "manifest signature invalid";
var version = parseInt(String(manifest.version || "0"), 10); var version = parseInt(String(manifest.version || "0"), 10);
if (isNaN(version) || version <= 0) throw "invalid manifest version"; if (isNaN(version) || version <= 0) throw "invalid manifest version";
if (version < MIN_TRUSTED_MANIFEST_VERSION) throw "manifest below minimum trusted version: remote=" + version + ", min=" + MIN_TRUSTED_MANIFEST_VERSION;
var localVersion = getTrustedVersion(); var localVersion = getTrustedVersion();
if (localVersion > 0 && version < localVersion) throw "manifest rollback: remote=" + version + ", local=" + localVersion; if (localVersion > 0 && version < localVersion) throw "manifest rollback: remote=" + version + ", local=" + localVersion;
__trustedManifest = manifest; __trustedManifest = manifest;
__securityStatus = { ok: true, msg: "安全清单验签通过version=" + version, version: version }; __securityStatus = { ok: true, msg: "安全清单验签通过version=" + version + ", keyId=" + keyId, version: version, keyId: keyId };
writeLog(__securityStatus.msg); writeLog(__securityStatus.msg);
return manifest; return manifest;
} catch (e) { } catch (e) {
@@ -393,6 +408,8 @@ var __out = (function() {
msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败", msg: started ? (rawMsg ? ("ToolHub 启动成功:" + rawMsg) : "ToolHub 启动成功") : "ToolHub 启动失败",
securityMsg: __securityStatus.msg, securityMsg: __securityStatus.msg,
manifestVersion: __securityStatus.version || 0, manifestVersion: __securityStatus.version || 0,
manifestKeyId: __securityStatus.keyId || "",
minManifestVersion: MIN_TRUSTED_MANIFEST_VERSION,
syncMsg: syncInfo.msg, syncMsg: syncInfo.msg,
updatedCount: syncInfo.count, updatedCount: syncInfo.count,
updatedModules: syncInfo.modules, updatedModules: syncInfo.modules,

1
ToolHub.js.sha256 Normal file
View File

@@ -0,0 +1 @@
279b1a5e7a9dee01bbfc8c67fab650285e9e1a899ec91f4fee08ac569287a393 ToolHub.js

View File

@@ -66,6 +66,7 @@
"size": 11125 "size": 11125
} }
}, },
"schema": 1, "keyId": "toolhub-targets-2026-rsa3072",
"version": 20260507152251 "schema": 2,
"version": 20260507154625
} }

View File

@@ -1 +1 @@
bXIc036dCe2RPuVJUV3QJO5Q3FZ3kc08337NRrjtYC03xV7evj7kYb1aa2jBadL6IaDSypE1BNtX4OzN0qDTXf4PxfPDVGcJrGLVJcipk+HpMDx2h2hlZZQq5A+1wx5R4XeA2NrMvclcUsLL2elRH/SzBr7pgR93hkzDMAJwJjyZrMUO2aa40EdmHWcFjNmYf3GzLd/8JD3IdRmi/WrVaikggnln2Pd0bsBNKJ1C3HGIKf+ZpZ2nFH5Y2NLgrvfDy+WVYiE3n4qnk8iwYGfEw3XqGG6zQy8QzrXVHmUyhQQ5YvqbcA79xmI9mqu7tEkXXcr3DLQBI0RZ5uYJGDxVmUVz0in2Mx/eBwPBpL4ivQ991BPy2ye8hZBt1WuXBCn8CRb00RL5P2iMUpEFv5b534Kw28JJap/T2KZQ8UZmODk8DWDQ6LOQfAmEShdrP7PqK/3uYYG0493uHqCD6BKpWo6YEnLvi2F/1DR9gPwHuoqjysF6gRNmKPAbQk5/BVv+ HleQZVymedTYdJbRyd2SNcndHqUMH1FuVjy7MJi9NQtgZKVNdKRBwns9w6le7cZZW8a2Xb3fqqlsBjJaDYQ3x/LveeE18u2SfNRoC97Q0lDVj9sxPRD/vsw6HQgDDuCMdqezmfWlROobd5CHHAjJvmaV+zHH6A3VYga6tAnY3/4JwVcoDua2W1Sjk3PB2Lqi9YcIGy3ub2gfm+RKakyBvYy9k/8cb7KbUbovPDU4oviS3Xn26aF3lN3o8tbTWBg9XYgsXM3gAqWZ+Y0YVXuZZB4VjgDw9cUITlZ8vxCxloo66nJSGy7/7CwDJ69F0i+vbipibracv2wut/RMqqkUaGy+PSd+EVFUFuLxoMFTs5eT4ybiSqOfj0/wdRfv//8XNa/s9sMZru+/PgKyNmSMTivAcdxjip43YNvRd/uWtQZTaHC72iVlHEeqNSPAR5gHPWcKuJ19EPecuVRr3wPK9i8Ois4QCpgdibCJ7cE2341x56ertHJQRAfjTN/UJ7eb

View File

@@ -1,13 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Generate ToolHub signed manifest. """Generate ToolHub signed manifest and entry checksum.
Requires openssl CLI. Private key is kept outside the repo by default: Security notes:
~/.hermes/toolhub_signing/private_key.pem - Private key stays outside the repo by default: ~/.hermes/toolhub_signing/private_key.pem
- The script prints git status/diff summary before signing. Use --yes in automation.
""" """
import argparse
import base64 import base64
import hashlib import hashlib
import json import json
import subprocess import subprocess
import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -16,6 +19,9 @@ CODE_DIR = ROOT / "code"
PRIVATE_KEY = Path.home() / ".hermes" / "toolhub_signing" / "private_key.pem" PRIVATE_KEY = Path.home() / ".hermes" / "toolhub_signing" / "private_key.pem"
MANIFEST = ROOT / "manifest.json" MANIFEST = ROOT / "manifest.json"
SIG = ROOT / "manifest.sig" SIG = ROOT / "manifest.sig"
ENTRY = ROOT / "ToolHub.js"
ENTRY_SHA = ROOT / "ToolHub.js.sha256"
DEFAULT_KEY_ID = "toolhub-targets-2026-rsa3072"
MODULES = [ MODULES = [
"th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js", "th_01_base.js", "th_02_core.js", "th_03_icon.js", "th_04_theme.js",
@@ -34,9 +40,43 @@ def sha256_file(path: Path) -> str:
return h.hexdigest() return h.hexdigest()
def git_output(args):
try:
return subprocess.check_output(["git", *args], cwd=ROOT, text=True, stderr=subprocess.STDOUT)
except Exception as e:
return f"<git {' '.join(args)} failed: {e}>\n"
def print_review_summary() -> None:
print("== ToolHub signing review ==")
print("-- git status --")
status = git_output(["status", "--short"])
print(status.rstrip() or "clean")
print("-- git diff --stat --")
diff_stat = git_output(["diff", "--stat"])
print(diff_stat.rstrip() or "no unstaged diff")
print("-- staged diff --stat --")
staged = git_output(["diff", "--cached", "--stat"])
print(staged.rstrip() or "no staged diff")
def main() -> None: def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--yes", action="store_true", help="skip interactive confirmation after review summary")
ap.add_argument("--key-id", default=DEFAULT_KEY_ID)
ap.add_argument("--version", type=int, default=0, help="manifest version; default UTC yyyyMMddHHmmss")
args = ap.parse_args()
if not PRIVATE_KEY.exists(): if not PRIVATE_KEY.exists():
raise SystemExit(f"Private key not found: {PRIVATE_KEY}") raise SystemExit(f"Private key not found: {PRIVATE_KEY}")
if not ENTRY.exists():
raise SystemExit(f"Entry file not found: {ENTRY}")
print_review_summary()
if not args.yes:
ans = input("Sign this manifest? Type YES to continue: ").strip()
if ans != "YES":
raise SystemExit("aborted")
files = {} files = {}
for name in MODULES: for name in MODULES:
@@ -48,9 +88,11 @@ def main() -> None:
"size": path.stat().st_size, "size": path.stat().st_size,
} }
version = args.version or int(time.strftime("%Y%m%d%H%M%S", time.gmtime()))
manifest = { manifest = {
"schema": 1, "schema": 2,
"version": int(time.strftime("%Y%m%d%H%M%S", time.gmtime())), "version": version,
"keyId": args.key_id,
"alg": "SHA256withRSA", "alg": "SHA256withRSA",
"files": files, "files": files,
} }
@@ -61,8 +103,15 @@ def main() -> None:
"openssl", "dgst", "-sha256", "-sign", str(PRIVATE_KEY), str(MANIFEST) "openssl", "dgst", "-sha256", "-sign", str(PRIVATE_KEY), str(MANIFEST)
]) ])
SIG.write_text(base64.b64encode(sig_bin).decode("ascii") + "\n", encoding="utf-8") SIG.write_text(base64.b64encode(sig_bin).decode("ascii") + "\n", encoding="utf-8")
entry_hash = sha256_file(ENTRY)
ENTRY_SHA.write_text(f"{entry_hash} ToolHub.js\n", encoding="utf-8")
print("== signed manifest ==")
print(f"manifest_version={manifest['version']}") print(f"manifest_version={manifest['version']}")
print(f"key_id={manifest['keyId']}")
print(f"signed_files={len(files)}") print(f"signed_files={len(files)}")
print(f"ToolHub.js_sha256={entry_hash}")
if __name__ == "__main__": if __name__ == "__main__":