# 程序接入指南 ## 1. 接入前准备 本更新站只提供私有更新服务,没有公开下载链接。其他程序需要通过应用层双向 RSA 签名认证,管理员账号密码不能代替客户端认证。 - API 基地址:当前站点的最终 HTTPS 源地址,例如 `https://updates.example.com`。不要包含尾斜杠、路径或查询参数;程序只支持部署在域名根路径。本机开发可以使用 `http://127.0.0.1:8088`。 - `programId`:后台“程序与版本”里创建的程序标识,例如 `com.example.desktop`。长度 2–64,使用小写字母、数字、点、下划线或横线,以小写字母或数字开头。 - `clientId`:后台“客户端授权”登记公钥后分配的 32 位小写十六进制 ID,不是程序标识或公钥指纹。 - 客户端私钥:可在后台“添加客户端”弹窗中一键生成 RSA 3072 位密钥对,然后下载私钥;也可在客户端本地生成并导入公钥。私钥只在管理员浏览器中生成、显示和导出,不提交或保存在更新站。不同授权身份使用不同密钥,同一身份允许多台机器共享。 - 服务端公钥:从后台“认证与密钥”导出,通过可信渠道送到客户端并核对指纹。不要依赖本页面或未经认证的 API 动态获取并首次信任公钥。 - 在后台为该客户端勾选允许访问的程序,启用客户端和程序,上传版本并设为当前发布版本。 - 保持两端系统时钟准确。默认挑战有效期 90 秒,客户端请求时间偏差允许 60 秒;实际值由服务端配置决定。 ## 2. 最小完整调用流程 ```text 客户端生成 32 字节随机数 → POST /api/v1/challenge → 验证服务端签名、指纹、上下文与挑战有效期 → 用客户端私钥签名 POST /api/v1/updates → 验证更新响应,读取 release 和 updateAvailable → 自行判断是否允许更新或降级 → 重新获取并验证一个新挑战 → 用客户端私钥签名 POST /api/v1/download → 暂存文件,验证签名清单、文件大小、SHA-256 → 全部成功后保存最终文件 ``` 挑战是一次性的,检查更新和下载不能共用;每次重试都要重新获取挑战。下载并不解压、执行或安装程序,安装器的安全校验由接入方负责。 ## 3. 三个客户端接口 - `POST /api/v1/challenge`:JSON 包含 `clientId` 和本次随机 `nonce`;无须客户端签名,但必须验证服务端返回的签名挑战。 - `POST /api/v1/updates`:JSON 包含 `programId` 和 `currentVersion`(未知当前版本可传空字符串);请求带四个认证头。响应为签名 JSON 信封,验证后才能读取更新信息。 - `POST /api/v1/download`:JSON 包含 `releaseId`;用新的挑战签名。成功响应为文件流,签名清单位于 `X-Update-Envelope` 响应头。 所有接口使用 `Content-Type: application/json`。正文先序列化一次为 UTF-8 字节,用这同一份字节计算 SHA-256、签名和发送;不要签名后再改变空格、字段顺序或中文编码方式。完整请求与响应示例、逐字段定义及签名原文见后面的“双向签名更新协议 v1”。 ## 4. 在弹窗生成密钥并导出接入配置 1. 后台“客户端授权” → “添加客户端”,填写名称并点击“一键生成密钥对”。 2. 浏览器生成匹配的公私钥,不是从公钥反推出私钥。公钥格式为 SPKI PEM(`BEGIN PUBLIC KEY`),私钥格式为 PKCS#8 PEM(`BEGIN PRIVATE KEY`);算法为 RSA 3072 位 + SHA-256 + PKCS#1 v1.5。 3. 下载或复制私钥,并勾选“我已保存私钥”;选择该身份可访问的程序后提交。提交接口只有名称、公钥和授权列表,没有私钥。 4. 创建成功后点击“下载完整接入配置 JSON”。配置包含 `protocol / algorithm / origin / clientId / clientPublicKey / clientPrivateKey / clientPublicKeyFingerprint / serverPublicKey / serverPublicKeyFingerprint / programs / clientName`。 5. 将 JSON 通过可信渠道交给客户端,或按字段映射到你的程序配置。同一份 JSON 可以分发给多台机器。服务器只保留公钥;关闭弹窗后无法从更新站找回私钥或再次导出含私钥的配置。 使用导出的配置直接检查或下载更新: ```sh php client/update.php --config=client-客户端ID.json --program=com.example.desktop --current-version=1.0.0 --check-only php client/update.php --config=client-客户端ID.json --program=com.example.desktop --current-version=1.0.0 --out=downloads ``` `programs` 是导出时的授权快照,最终访问权限仍由服务器实时判断。不要把 JSON 放到公开下载目录;服务端公钥信任来自管理员提供的配置,而不是运行时从未认证 API 自动信任。 ## 5. 使用项目内置 PHP 客户端 安装包中的 `client/UpdateClient.php` 已实现完整认证、响应验签、下载摘要校验及临时文件处理。`client/update.php` 是命令行入口。它们依赖同一项目中的 `src/core.php`,不是两个独立的单文件脚本;复制到其他项目时保留 `client/` 与 `src/core.php` 的目录关系。需要 PHP 的 curl 和 openssl 扩展;纯客户端调用不需要连接本站数据库。 在客户端机器生成密钥,只把 `client-public.pem` 导入后台: ```sh php bin/keygen.php client/keys ``` 只检查更新,替换域名、客户端 ID、程序 ID 和公钥路径: ```sh php client/update.php --url=https://updates.example.com --id=替换为32位客户端ID --private-key=client/keys/client-private.pem --server-key=server-public.pem --program=com.example.desktop --current-version=1.0.0 --check-only ``` 下载已验证的更新包,默认拒绝降级,不会解压或运行: ```sh php client/update.php --url=https://updates.example.com --id=替换为32位客户端ID --private-key=client/keys/client-private.pem --server-key=server-public.pem --program=com.example.desktop --current-version=1.0.0 --out=downloads ``` 在现有 PHP 程序中调用: ```php check('com.example.desktop', $currentVersion); if ($update['updateAvailable'] && $update['release']) { $release = $update['release']; if (version_compare($release['version'], $currentVersion, '<')) { throw new RuntimeException('拒绝自动降级'); } $file = $client->download($release, __DIR__ . '/downloads'); // 至此文件已完成验签、大小与 SHA-256 校验;自行决定后续安装流程。 } ``` `check()` 和 `download()` 内部会各自获取新挑战,不要自行复用挑战。调用方需要捕获异常并记录失败;服务端验签失败、授权失效或网络中断时不能继续使用未验证的元数据或临时文件。 ## 6. 在其他语言中实现 - 请求签名:SHA-256 + RSA PKCS#1 v1.5,不使用 RSA-PSS。 - Base64 编码:base64url,使用 `-` 与 `_`,去掉末尾 `=`;解码时可补齐填充。 - 客户端签名原文:按后面的协议拼接 8 行,以单个 LF 连接,末尾不加 LF。时间戳使用十进制 Unix 秒字符串,正文摘要使用小写十六进制。 - 服务端验签原文:`UPDATE-STATION-RESPONSE-V1`、一个 LF、信封中的 `payload` 原样字符串;不是解码后的 JSON。 - 服务端验签通过后再解码 payload,并校验 `protocol / kind / status / issuedAt / context`。context 中 HTTP 方法、路径、客户端 ID、挑战 ID 和正文摘要必须与本次请求一致。 - 挑战响应的 `context.clientId` 与 `context.challengeId` 通常为空字符串(正常请求不带认证头);实际客户端 ID 和客户端随机数绑定在 `data.clientId / data.clientNonce`。更新与下载的 context 使用认证头中的 ID。 - 下载清单先将 `X-Update-Envelope` 做一次 base64url 解码得到信封 JSON,再按同样规则验签。下载全过程暂存为临时文件,禁止跳过清单或文件摘要校验。 - 新上传的 `release.filename` 为版本号加压缩格式后缀,例如 `4.0.1.zip`、`4.0.1.tar.gz`,不会沿用管理员所选原文件名。历史版本可能仍使用原名;实际以验签后的元数据为准。 - 版本不是严格的语义化版本;服务端的 `updateAvailable` 只表示与当前发布版本不同。自行制定升级、降级及版本比较策略。 ## 7. 多台机器共享同一授权身份 可以共享:每台机器使用相同的 `clientId`、客户端私钥及预置服务端公钥。协议不绑定单台机器或固定 IP;这是一份“程序/分发渠道授权身份”,不是每台机器的设备许可证。 - 每台机器自行生成客户端随机数并获取新挑战,挑战对象不得通过共享文件、缓存或其他机器复用。可以并发请求,只要各自的挑战没有过期或被消耗。 - 修改授权或停用该身份会影响所有共用机器。客户端列表统计的是授权身份数量;最近认证时间是这些机器中最近一次认证,不是设备数量。 - 默认同一 clientId 每分钟最多请求 30 个挑战,所有共用机器合计;同一来源 IP 每分钟最多 90 个挑战和 180 次签名认证。一次检查更新需要一个挑战;若要下载,另外需要一个挑战。 - 分发数量较大时,客户端错峰检查并加入随机延迟,遇到 429 尊重 `Retry-After`。默认限额不变,但可以在 `config/local.php` 中按实际负载配置以下可选整数(范围 1–100000);老配置未添加时仍使用原默认值。 ```php 'challenge_client_per_minute' => 300, // 同一授权身份所有机器的挑战总额 'challenge_ip_per_minute' => 300, // 同一来源 IP 的挑战总额 'verify_ip_per_minute' => 600, // 同一来源 IP 的签名请求总额 ``` 共享模式仍没有公网直链:普通浏览器或未签名请求不能下载。持有分发私钥的人能够以该共享身份访问其获授权的程序,这是该模式的边界。 ## 8. 常见接入问题 - 401:检查登记公钥是否对应本机私钥、客户端是否启用、系统时间、挑战是否过期或已消耗、签名正文是否与实际发送字节一致。 - 403:检查客户端是否获得该程序授权,以及程序是否启用。 - `release=null`:没有启用的当前发布版本,需要后台上传并发布。 - 签名失败:确认使用 PKCS#1 v1.5、base64url、LF 换行、无末尾换行,且响应签名针对 payload 原样字符串。 - 下载失败:检查响应头是否被代理删除、文件大小和摘要是否一致;丢弃临时文件后用新挑战重试。 - 429:限流,遵守 `Retry-After`,避免高频轮询与无间隔重试。 - 503 或非 JSON 响应:站点或代理不可用,安全失败。不得因错误响应没有签名而改为信任未认证内容。 ## 9. 文档读取地址 - `/docs`:本页,浏览器可直接阅读,无需登录。 - `/docs/integration.md`:本接入指南原文,适合其他程序或 AI 工具读取。 - `/docs/protocol.md`:完整协议原文,包含签名格式、请求响应 JSON、下载校验和错误规则。 这些地址只公开随程序打包的文档,不提供私钥、后台凭据、真实客户端列表或公开程序包链接。生产 API 建议由桌面程序、服务端或更新代理调用;当前未提供浏览器跨域 CORS 接入。