security: harden ToolHub update trust model
This commit is contained in:
27
ToolHub.js
27
ToolHub.js
@@ -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
1
ToolHub.js.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
279b1a5e7a9dee01bbfc8c67fab650285e9e1a899ec91f4fee08ac569287a393 ToolHub.js
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
"size": 11125
|
"size": 11125
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": 1,
|
"keyId": "toolhub-targets-2026-rsa3072",
|
||||||
"version": 20260507152251
|
"schema": 2,
|
||||||
|
"version": 20260507154625
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
Reference in New Issue
Block a user