feat(sec): 二期签名鉴权 2A 最小闭环 (agent-185) — meow_verify(Ed25519验签CLI,公钥固化,TweetNaCl+SHA256单文件) + meow_sign(宿主keygen/sign,尾部180B footer签名块,私钥仅.keys/不入库) + nyai/meow_pkg_install 验签分级(有签必验/os.meow.*无签拒装/第三方直装告警/三重绑定/原签存.mpk.src导出保留) + svc_power IPC鉴权(SO_PEERCRED→/proc/pid/exe→包名/系统身份allowlist,系统动作仅os.meow.*或系统身份) + cmd_upgrade同步签名分级防绕过

meow_verify 37e50606; nyai f2938d28; svc_power e0494eb7; launcher 24c9ee5d(含热重载)
宿主验证12项全过(正例/篡负载/篡包名/无签系统包拒/第三方直装/降级拒/导出round-trip byte-identical/verify缺失宁缺勿装/footer容忍/E2E PERM/鉴权分类)
出入: TweetNaCl替orlp/ed25519(任务允许同等实现); 读级暂仍uid==0(用户随后定案放宽到任意调用者,待改); 系统身份allowlist为启发式
This commit is contained in:
MeowOS committed 2026-08-31 04:33:46 +08:00
1 parent f50abae02a
commit 3f57d7fd4f
22 files changed
+1978 -15

No files matched your search

+115 -1
View File
@@ -506,6 +506,82 @@ static int meow_app_prepare(const char *exec)
}
/* ---- II 期签名校验 / 导出保留签名(MPK_SIGNATURE_DESIGN §5.1/§5.4,shell-out meow_verify) ---- */
/* meow_verify --system <mpk> --package <pkg>。返回 0=有效签 /1=有签不通过 /2=无签名 /127=工具不可用。 */
static int pkg_verify_sig(const char *mpk, const char *pkg)
{
const char *vb = getenv("MEOW_VERIFY_BIN");
if (!vb || !*vb) vb = "/usr/bin/meow_verify";
char cmd[700];
snprintf(cmd, sizeof cmd, "'%s' --system '%s' --package '%s' >/dev/null 2>&1", vb, mpk, pkg);
int rc = system(cmd);
if (rc == -1) return 127;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 127;
}
/* 读已解压 app.json 的某字符串字段(用 launcher 的 json_str;返回 0 成功)。 */
static int pkg_json_field(const char *appjs, const char *key, char *out, int n)
{
if (!appjs || !key || n <= 0) return -1;
FILE *f = fopen(appjs, "r");
if (!f) return -1;
static char j[16384];
size_t r = fread(j, 1, sizeof j - 1, f);
fclose(f);
j[r] = 0;
if (!r) return -1;
if (json_str(j, key, out, n) <= 0) { out[0] = 0; return -1; }
return 0;
}
/* 读 .mpk 内 app.json 的某字符串字段(gzip -dc | tar -xO app.json;返回 0 成功)。 */
static int pkg_json_field_mpk(const char *mpk, const char *key, char *out, int n)
{
char cmd[700];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -xO app.json 2>/dev/null", mpk);
FILE *pf = popen(cmd, "r");
if (!pf) return -1;
static char j[16384];
size_t r = fread(j, 1, sizeof j - 1, pf);
pclose(pf);
if (!r) return -1;
j[r] = 0;
if (json_str(j, key, out, n) <= 0) { out[0] = 0; return -1; }
return 0;
}
/* 安装成功后,把原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名)。 */
static void pkg_save_src(const char *mpk, const char *pkg)
{
char dst[320], cmd[720];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' 2>/dev/null && "
"cp '%s' '%s' 2>/dev/null", pkg, mpk, dst);
if (system(cmd) != 0) logmsg("pkg_install: save .mpk.src fail for %s", pkg);
}
/* 导出保留签名:缓存 .mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装 → 直接复制(不二次签名)。
* 成功 0;否则 -1(调用方现场重打包,无签名)。 */
static int pkg_export_preserve_sig(const char *pkg, const char *dst)
{
char srcp[320], instj[280], cmd[760], tmp[340];
snprintf(srcp, sizeof srcp, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
if (access(srcp, R_OK) != 0) return -1;
if (pkg_verify_sig(srcp, pkg) != 0) { logmsg("pkg_export: cached .mpk.src sig fail %s", pkg); return -1; }
char cv[64], iv[64];
snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(instj, "version", iv, sizeof iv) != 0) return -1;
if (pkg_json_field_mpk(srcp, "version", cv, sizeof cv) != 0) return -1;
if (strcmp(iv, cv) != 0) { logmsg("pkg_export: cached version %s != installed %s", cv, iv); return -1; }
snprintf(tmp, sizeof tmp, "%s.tmp", dst);
snprintf(cmd, sizeof cmd, "cp '%s' '%s' 2>/dev/null", srcp, tmp);
if (system(cmd) != 0 || access(tmp, R_OK) != 0) return -1;
if (rename(tmp, dst) != 0) { unlink(tmp); return -1; }
return 0;
}
/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs/<pkg>/。
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{data,cache})。
@@ -525,9 +601,25 @@ int meow_pkg_install(const char *mpk_path)
if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; }
memcpy(pkg, bn, len); pkg[len] = 0;
/* 目标 MeowAPPs/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 app.json */
char dst[256], bak[256], cmd[700];
char dst[256], bak[256], cmd[760];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid());
int replacing = (access(dst, F_OK) == 0);
/* II 期:签名分级先于解包(MPK_SIGNATURE_DESIGN §5.2:先验签再解包,失败不落盘)。
* 有签名必须验签(失败拒装);os.meow.* 必须有有效签名;无签名第三方放行(告警)。 */
int vr = pkg_verify_sig(mpk_path, pkg);
int sysp = (strncmp(pkg, "os.meow.", 8) == 0);
if (vr == 1) { logmsg("pkg_install: sig invalid, reject %s", pkg); return -3; }
else if (vr == 2) {
if (sysp) { logmsg("pkg_install: sys pkg %s must be signed, rejected", pkg); return -3; }
logmsg("pkg_install: %s unsigned (3rd party allowed)", pkg);
}
else if (vr != 0) {
if (sysp) { logmsg("pkg_install: meow_verify unavailable rc=%d, sys pkg reject", vr); return -3; }
logmsg("pkg_install: meow_verify unavailable rc=%d, %s unverified (3rd party)", vr, pkg);
}
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
snprintf(cmd, sizeof cmd,
"rm -rf '%s' 2>/dev/null; "
@@ -535,10 +627,27 @@ int meow_pkg_install(const char *mpk_path)
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
/* II 期:app.json.package 绑定(== 包名,禁改包名/目录改名;防第三方面具系统包)。 */
{
char appj[280], ph[64];
snprintf(appj, sizeof appj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(appj, "package", ph, sizeof ph) == 0 && strcmp(ph, pkg) != 0) {
logmsg("pkg_install: app.json.package %s != %s, reject", ph, pkg);
char rb[760];
snprintf(rb, sizeof rb, "rm -rf '%s' 2>/dev/null; [ -d '%s' ] && mv '%s' '%s' 2>/dev/null",
dst, bak, bak, dst);
system(rb);
return -3;
}
}
/* 数据 init:MeowData/<pkg>/{data,cache} */
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
system(cmd);
/* 有效签 → 原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
if (vr == 0) pkg_save_src(mpk_path, pkg);
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
return 0;
}
@@ -556,6 +665,11 @@ int meow_pkg_export(const char *pkg, const char *out_path)
snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; }
snprintf(dst, sizeof dst, "%s", out_path);
/* II 期:导出保留签名 —— 若安装时存了原签 .mpk.src 且其版本==已装 → 直接复制(不二次签名)。 */
if (pkg_export_preserve_sig(pkg, dst) == 0) {
logmsg("pkg_export: %s -> %s (signed, preserved)", pkg, dst);
return 0;
}
/* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */
char cmd[512];
snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && "