Files
YwxAppThink/scripts/lib/package_signature.php
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0)
- 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、
  ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签
  (ywxapp.framework_package_verify,默认开启)
- JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret()
  校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET
- /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验
- 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流
- TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive
- 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php)
- 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis
- paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG

工程化 / 交付
- 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本
- 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过
- 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障
- .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess

结构
- appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

98 lines
3.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 发布包签名公共库(供 scripts/ 下的打包脚本复用)
*
* 为什么单独抽一层:打包脚本是无框架的独立 CLI,而验签服务 ywxapp\service\PackageVerifier
* 需要被运行期代码复用;两者算法的任何差异都会导致「打得出来、装不进去」,
* 故由本库薄薄一层接过去,保证签名/验签永远同一份实现。
*
* 依赖:仅 openssl 扩展 + ZipArchive,不需要启动框架。
*
* @package scripts
*/
declare(strict_types=1);
require_once dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'ywxapp' . DIRECTORY_SEPARATOR
. 'service' . DIRECTORY_SEPARATOR . 'PackageVerifier.php';
use ywxapp\service\PackageVerifier;
/**
* 定位打包私钥(按优先级):
* 1) 环境变量 PACKAGE_SIGN_KEY(私钥文件路径)
* 2) runtime/package_private.pem
* 3) build/package_private.pem
*
* 私钥永远不入库:.gitignore 已覆盖 /runtime 与 /build。
*
* @param string $root 项目根
* @return string 私钥文件路径;不存在时返回 ''
*/
function pkResolvePrivateKey(string $root): string
{
$env = trim((string) (getenv('PACKAGE_SIGN_KEY') ?: ''));
if ($env !== '' && is_file($env)) {
return $env;
}
foreach ([
$root . 'runtime' . DIRECTORY_SEPARATOR . 'package_private.pem',
$root . 'build' . DIRECTORY_SEPARATOR . 'package_private.pem',
] as $candidate) {
if (is_file($candidate)) {
return $candidate;
}
}
return '';
}
/**
* 对一个已打包完成的 zip 签名(追加 signature.json)
*
* @param string $zipPath zip 绝对路径
* @param string $version 版本号(参与签名,防降级)
* @param string $privateKey 私钥文件路径;为空则跳过并返回 null(仅本机自测可接受)
* @return array|null 签名信息 ['version','sha256','files','signed_at','signature'] 或 null(未签名)
*/
function pkSignPackage(string $zipPath, string $version, string $privateKey): ?array
{
if (! is_file($zipPath)) {
throw new RuntimeException("待签名包不存在:{$zipPath}");
}
if ($privateKey === '' || ! is_file($privateKey)) {
fwrite(STDERR, "[签名] 未找到签名私钥,跳过:{$zipPath}\n"
. " 上线发布前请执行 php scripts/package_keygen.php 生成密钥对。\n");
return null;
}
$info = PackageVerifier::sign($zipPath, $privateKey, $version);
return $info;
}
/**
* 校验一个包的签名(用于发布前自检)
*
* @param string $zipPath
* @param string $publicKey 公钥 PEM 内容或文件路径
* @param string $version 期望版本;留空不校验版本
* @return array 签名信息
*/
function pkVerifyPackage(string $zipPath, string $publicKey, string $version = ''): array
{
$pem = is_file($publicKey) ? (string) file_get_contents($publicKey) : $publicKey;
return PackageVerifier::verifyFile($zipPath, $pem, $version);
}
/**
* 输出一行人类可读的签名状态(供打包脚本打印)
*
* @param array|null $sig pkSignPackage() 的返回值
*/
function pkSignLabel(?array $sig): string
{
if ($sig === null) {
return '**未签名**(禁止对外发布;请先执行 php scripts/package_keygen.php)';
}
return '已签名 RSA-SHA256(' . ($sig['files'] ?? 0) . ' 个文件,'
. date('Y-m-d H:i:s', (int) ($sig['signed_at'] ?? 0)) . ')';
}