Aspire CLI 安全 npm 全局工具安装:基于 Sigstore 与 SLSA 的供应链验证设计
2026/9/17 5:26:51 网站建设 项目流程

Aspire CLI 安全 npm 全局工具安装:基于 Sigstore 与 SLSA 的供应链验证设计

【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire

导读

Aspire CLI 在aspire agent init过程中会把@playwright/clinpm 包作为全局工具安装到用户机器上。由于该工具将以用户完整权限运行,安装前必须严格核验其真实性与来源。本文基于仓库中的安全设计文档(docs/specs/safe-npm-tool-install.md),完整解析其威胁模型、六步验证流程、信任锚点、残余风险、实现常量与配置项,并结合 PlaywrightCliInstaller.cs、SigstoreNpmProvenanceChecker.cs 等源码深入说明每一步背后的工程实现。读完本文,你将理解为什么"从官方源下载 + TLS"并不足以保障供应链安全,以及如何通过 Sigstore(Fulcio + Rekor)+ SLSA 构建证明把"声称的来源"变成"可密码学验证的来源"。


一、背景:为什么需要"安全安装"而不是"直接 npm install -g"

1.1 应用场景

当用户运行aspire agent init并选择 Playwright CLI 技能时,Aspire CLI 需要把@playwright/cli安装为全局工具,随后调用playwright-cli install --skills生成 agent 技能文件。这一流程由 AgentInitCommand.cs 的 Phase 4 触发,核心实现位于 PlaywrightCliInstaller.cs。

问题在于:这个工具安装后会以当前用户的完整权限运行。如果安装的包被篡改、被替换或来自恶意来源,攻击者就等于在用户机器上植入了提权后的可执行代码。因此,Aspire 没有走"从注册表拉取最新版直接npm install -g"的常规路径,而是设计了一条完整的验证链路。

1.2 为什么不能只依赖 HTTPS

"从https://registry.npmjs.org/下载"只能保证传输过程不被窃听或篡改(传输安全),却无法回答三个更根本的问题:

  1. 这个包确实是维护者发布的那一份吗?(发布安全)
  2. 构建这个包的源码确实来自预期的开源仓库吗?(来源安全)
  3. 触发构建的 CI 流水线确实是官方那条吗?(流程安全)

要回答这些问题,就需要密码学签名与可验证的构建元数据——这正是本文要展开的验证流程所做的事情。


二、威胁模型:保护什么,不保护什么

2.1 要防御的攻击场景

威胁描述
注册表沦陷(Registry compromise)攻击者获得 npm registry 的写入权限,发布一个恶意的@playwright/cli版本
发布令牌失窃(Publish token theft)攻击者窃取维护者的 npm publish token,发布被篡改的包
中间人攻击(Man-in-the-middle)攻击者拦截网络请求,用另一个 tarball 顶替目标包
依赖混淆(Dependency confusion)名称相近的恶意包被安装,冒名顶替预期包

2.2 明确不防御的范围

设计文档同时划出了边界——以下场景不在本机制的保护范围内:

  • 合法源码仓库(microsoft/playwright-cli)本身被攻破;
  • GitHub Actions 构建基础设施(即 Sigstore 的 OIDC provider)被攻破;
  • Sigstore 透明日志基础设施被攻破;
  • 通过@playwright/cli的合法依赖引入的恶意代码(部分缓解,见"残余风险"章节)。

2.3 信任锚点

整条验证链最终依赖以下四个信任锚点:

信任锚点提供什么如何保护
npm registry(registry.npmjs.org)包元数据、tarball 托管HTTPS/TLS,公共 npm 注册表
Sigstore(Fulcio + Rekor)密码学签名的 attestation支持 OIDC 联邦的公共 CA,追加式透明日志,通过 Sigstore .NET 库在进程内验证(基于 TUF 信任根)
GitHub Actions OIDCSigstore 证书中的构建者身份声明GitHub 自身的基础设施安全
硬编码的预期值包名、版本范围、预期源码仓库代码审查 + 本项目自己的发布流程

关键点在于:信任锚点中只有最后一类是"我们自己的预期值",其余都来自第三方基础设施。验证流程要做的,就是把来自第三方的声明与硬编码的预期值逐项比对。


三、六步验证流程

Step 1:解析包版本

动作:对公共 npm registry(https://registry.npmjs.org/)执行npm view @playwright/cli@{versionRange} version。默认版本范围为>=0.1.3,解析为不低于 0.1.3 的最新已发布版本;也可通过配置键playwrightCliVersion覆盖为指定精确版本。

这一步建立了什么:明确了要安装的确切版本。

信任基础:HTTPS/TLS 保护的公共 npm registry。

局限:如果注册表被攻破,它可能选择一个攻击者控制的版本。仅靠这一步远远不够。

源码印证:NpmRunner.cs 中ResolvePackageAsync隔离的临时目录中执行npm view ... --registry https://registry.npmjs.org/。显式传入--registry很关键:它确保解析和安装都走公共源,而不会继承项目级.npmrc指向的私有源(例如 Azure DevOps Artifacts 源,若未镜像完整依赖树会返回 401 导致aspire agent init失败)。当版本范围匹配多个版本时,npm 会按升序输出多行@scope/pkg@version 'version'格式,TryExtractLastVersion(NpmRunner.cs)会取最后一行(即最高版本)。

Step 2:检查是否已安装合适版本

动作:执行playwright-cli --version,与解析出的版本比较。

这一步建立了什么:能否直接跳过安装(已是最新或更新)。

信任基础:之前安装的二进制本身。如果用户系统已被攻破,这个结果可能被伪造,但这超出威胁模型范围。

源码印证:PlaywrightCliInstaller.cs 使用SemVersion.ComparePrecedence比较已安装版本与目标版本:若comparison >= 0则跳过安装,但仍会执行技能文件生成与镜像(防止技能文件缺失)。

顺带一提,版本覆盖值并非直接透传给 npm:SemVersion.TryParse(versionOverride, SemVersionStyles.Strict, ...)会先拒绝任何非严格 SemVer 2.0 的取值(如范围字符串>=1.0.0、dist-taglatest或任意字符串),防止畸形配置值被 npm 以意外方式解释(PlaywrightCliInstaller.cs)。

Step 3:下载 tarball 并计算哈希

动作:对公共 npm registry 执行npm pack @playwright/cli@{version},然后计算归档文件的SHA-512 SRI 值

这一步建立了什么:拿到了即将安装的本地归档文件的确切摘要值。

信任基础:该摘要本身在 Step 4 的签名 attestation 验证通过之前不被信任——这正是设计的关键:摘要值只在"被签名覆盖"之后才有意义。

源码印证ComputeIntegrity(PlaywrightCliInstaller.cs)对 tarball 计算sha512-{Base64(SHA512(data))};NpmRunner.cs 的PackAsync--pack-destination指定输出目录,并从 npm 输出中解析 tarball 文件名。下载发生在Directory.CreateTempSubdirectory("aspire-playwright-")创建的临时目录,安装完成后在finally块中递归清理(PlaywrightCliInstaller.cs)。

Step 4:验证 Sigstore attestation 与来源元数据

这是整个机制的核心步骤,动作序列为:

  1. https://registry.npmjs.org/-/npm/v1/attestations/@playwright/cli@{version}获取 attestation bundle;
  2. 找到predicateType: "https://slsa.dev/provenance/v1"(SLSA Build L3 来源证明)的 attestation;
  3. 从 attestation 的bundle字段提取 Sigstore bundle;
  4. 使用 Sigstore .NET 库 的SigstoreVerifier密码学验证 Step 3 的 SHA-512 摘要与 Sigstore bundle,VerificationPolicy配置为CertificateIdentity.ForGitHubActions("microsoft", "playwright-cli")
  5. Base64 解码 DSSE envelope payload,提取 in-toto statement;
  6. 校验 provenance predicate 中的以下字段:
字段Payload 中的位置预期值证明什么
源码仓库predicate.buildDefinition.externalParameters.workflow.repositoryhttps://github.com/microsoft/playwright-cli包由合法源码构建
工作流路径predicate.buildDefinition.externalParameters.workflow.path.github/workflows/publish.yml构建使用了预期的 CI 流水线,而非临时或被注入的工作流
构建类型predicate.buildDefinition.buildTypehttps://slsa-framework.github.io/github-actions-buildtypes/workflow/v1构建运行在 GitHub Actions 上,隐含确认 OIDC token 签发者为https://token.actions.githubusercontent.com
工作流 refpredicate.buildDefinition.externalParameters.workflow.ref通过调用方提供的回调验证(对@playwright/cli:kind=tags,name=v{version}构建由与包版本匹配的版本 tag 触发,而非任意分支或提交

关于工作流 ref:tag 格式是包相关的——不同包可能使用不同约定(如v0.1.10.1.1@scope/pkg@0.1.1)。ref 会被解析为结构化组件(WorkflowRefInfo),由调用方提供验证回调。具体到@playwright/cli,回调要求kind == "tags"且 name 等于{version}v{version}(PlaywrightCliInstaller.cs)。

这一步建立了什么:本地 tarball 摘要被密码学真实的 Sigstore bundle 覆盖;签名证书由 Sigstore 的 Fulcio CA 签发;签名记录在 Rekor 中;OIDC 身份与microsoft/playwright-cli的 GitHub Actions 工作流匹配;来源元数据同时确认了预期的仓库、工作流、CI 系统和版本 tag。

信任基础:Sigstore 的公钥基础设施(通过SigstoreTuf.NET 库实现),TUF 信任根自动下载并验证。即使 npm registry 被攻破,攻击者也伪造不了有效的 Sigstore 签名——他们需要攻破 Fulcio(Sigstore CA)或从 GitHub Actions 为合法仓库的工作流拿到有效 OIDC token。由于 Sigstore 验证与 provenance 字段校验在同一个 attestation bundle 上一次完成,签名验证与内容检查之间不存在 TOCTOU 空窗

为什么所有 provenance 字段都要校验:只校验 Sigstore 证书身份(GitHub Actions + 仓库)必要但不充分——对仓库有写权限的攻击者可以注入一个恶意工作流(如.github/workflows/evil.yml)。同时校验工作流路径、构建类型和工作流 ref,才能确保包是由特定预期的 CI 流水线发布 tag构建的。

附加提取字段(不直接用于验证):provenance 解析器还会从 attestation 提取runDetails.builder.id,该值出现在NpmProvenanceData结果中供日志与诊断使用,但当前不作为验证门禁。

源码印证:完整实现见 SigstoreNpmProvenanceChecker.cs。关键点包括:

  • 证书身份验证VerifySigstoreBundleAsync(L244-L289)先从预期仓库 URL 解析 owner/repo,再构建CertificateIdentity.ForGitHubActions(owner, repo)策略,通过TryVerifyDigestAsync校验摘要(SHA-512 分支,L295-L322)。
  • 包身份与摘要校验VerifyNpmSubject(L327-L371)检查 in-toto subject 的 PURL 必须精确等于pkg:npm/%40playwright/cli@{version}(scoped 包名的@被编码为%40,与 npm-package-arg 的生成方式一致),且 subject digest 中的 sha512 必须与本地 tarball 的 SRI 值一致——这一步把签名覆盖的 artifact磁盘上要安装的文件绑定。
  • 字段校验与失败门禁ProvenanceVerificationOutcome枚举(INpmProvenanceChecker.cs)为每个门禁定义了独立结果:AttestationFetchFailedAttestationParseFailedSlsaProvenanceNotFoundPayloadDecodeFailedPackageIdentityMismatchPackageDigestMismatchSourceRepositoryNotFoundSourceRepositoryMismatchWorkflowMismatchBuildTypeMismatchWorkflowRefMismatchVerified。任一失败都会让InstallCoreAsync立即返回PlaywrightInstallStatus.Failed(PlaywrightCliInstaller.cs)。
  • ref 解析WorkflowRefInfo.TryParse(INpmProvenanceChecker.cs)把refs/{kind}/{name...}拆成 kind 与 name,name 允许包含斜杠(如refs/tags/@scope/pkg@1.0.0)。
  • 深度防御ExtractProvenanceFromResult(SigstoreNpmProvenanceChecker.cs)优先使用 Fulcio 证书扩展中的SourceRepositoryUriSourceRepositoryRef(它们被密码学绑定到签名证书),其次才回退到 predicate 中的字段;VerifyProvenanceFields中源码仓库虽然已在证书扩展策略中验证过,仍再次比对以作深度防御(L458-L518)。

Step 5:从已验证的 tarball 全局安装

动作:执行npm install -g {tarballPath}将已验证的 tarball 安装为全局工具。

这一步建立了什么:工具已安装并出现在用户 PATH 上。

信任基础:此前所有验证步骤均已通过——Sigstore attestation 验证了本地归档摘要,并确认了正确的源码仓库、工作流与构建系统。

源码印证InstallGlobalAsync(NpmRunner.cs)执行的是npm install -g {tarballPath} --ignore-scripts --registry https://registry.npmjs.org/--ignore-scripts是刻意的:根 tarball 经过了 provenance 验证,但其传递依赖没有,该标志用于在安装期间禁止依赖生命周期脚本执行(详见"残余风险"章节)。

Step 6:生成并镜像技能文件

动作:执行playwright-cli install --skills在主技能目录(.claude/skills/playwright-cli/)生成 agent 技能文件,然后将技能目录镜像到所有检测到的其他 agent 环境技能目录(如.github/skills/playwright-cli/.opencode/skill/playwright-cli/)。镜像是一次完整同步——文件会被创建、更新,过期文件会被删除,确保所有环境拥有完全一致的技能内容。

这一步建立了什么:Playwright CLI 技能文件对所有已配置的 agent 环境可用。

源码印证:技能位置的完整清单见 SkillLocation.cs:标准位置.agents/skills/(VS Code、GitHub Copilot、OpenCode 通用,默认选中且支持用户级安装)、Claude Code 的.claude/skills/、VS Code / GitHub Copilot 的.github/skills/、OpenCode 的.opencode/skill/InstallAndMirrorSkillsAsync(PlaywrightCliInstaller.cs)会先快照所有技能目录的安装前存在状态,再执行安装,然后:

  • 把主目录内容同步到每个用户选中位置(跳过主目录本身),SyncDirectory(L419-L462)实现"目标与源完全一致";
  • 清理本次运行中在用户未选中位置新建的playwright-cli目录——但只删本次运行新建的,安装前已存在的内容绝不动RemoveEmptyParentDirectories还会沿路径向上清理空的父目录(深度受相对路径段数 + 1 限制,防止误删,L398-L413)。

镜像过程中若出现IOExceptionUnauthorizedAccessException,安装状态降级为InstalledWithWarnings而非直接失败(L290-L298),对应AgentInitCommand中不同状态的分支处理(AgentInitCommand.cs)。


四、验证链总览

设计文档给出了完整的验证链示意图,核心流程为:

┌──────────────────────────────┐ │ Hardcoded expectations │ │ • Package: @playwright/cli │ │ • Version range: >=0.1.1 │ │ • Source: microsoft/ │ │ playwright-cli │ │ • Workflow: .github/ │ │ workflows/publish.yml │ │ • Build type: GitHub Actions │ │ workflow/v1 │ └──────────────┬────────────────┘ │ ┌──────────────▼────────────────┐ │ Step 1: Resolve version │ │ from public registry │ └──────────────┬────────────────┘ │ ┌──────────────▼────────────────┐ │ Step 3: npm pack from │ │ public registry + SHA-512 │ └──────────────┬────────────────┘ │ ┌──────────────▼────────────────┐ │ Step 4: Verify digest + │ │ Sigstore provenance │ └──────────────┬────────────────┘ │ ┌──────────────▼────────────────┐ │ Step 5: npm install -g │ │ (from verified tarball) │ └───────────────────────────────┘

(注:Step 2 的"已安装版本检查"位于解析之后、下载之前,命中时直接跳到 Step 6 补齐技能文件。)

这条链路的精髓是把信任从"声称"转移到"证据":解析来源(Step 1)、锁定文件(Step 3)、密码学绑定签名(Step 4)三者叠加,即使注册表被攻破,攻击者也无法为恶意 tarball 伪造通过 Step 4 全部门禁的签名与来源元数据。


五、残余风险与缓解措施

1. Time-of-check-to-time-of-use(TOCTOU)

  • 风险:在版本解析与下载之间,feed 上的包可能被替换。
  • 缓解:Sigstore 验证的是实际安装的那个 tarball的 SHA-512 摘要,且安装使用的是同一个本地文件——验证对象与安装对象天然一致,不存在"验证 A、安装 B"的空窗。

2. 传递依赖攻击

  • 风险@playwright/cli的依赖可能被攻破。
  • 缓解--ignore-scripts标志禁止了安装脚本的执行。但依赖的代码在工具被调用时仍会运行——这部分仅由覆盖依赖树的 Sigstore attestation 部分缓解,对所有传递依赖做全面供应链验证不在当前范围内。

六、实现常量

设计文档给出了全部实现常量的清单,与 PlaywrightCliInstaller.cs 及 SigstoreNpmProvenanceChecker.cs 中的源码定义一致:

internal const string PackageName = "@playwright/cli"; internal const string VersionRange = ">=0.1.3"; private const string PublicRegistry = "https://registry.npmjs.org/"; internal const string ExpectedSourceRepository = "https://github.com/microsoft/playwright-cli"; internal const string ExpectedWorkflowPath = ".github/workflows/publish.yml"; internal const string ExpectedBuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1"; internal const string NpmRegistryAttestationsBaseUrl = "https://registry.npmjs.org/-/npm/v1/attestations"; internal const string SlsaProvenancePredicateType = "https://slsa.dev/provenance/v1";

这些"硬编码预期值"是整个信任链中唯一由本项目自己掌控的环节——它们通过代码审查和本项目自身的发布流程来保证正确性,是所有外部声明的比对基准。


七、配置项:两个"破窗"开关

设计文档提供了两个通过aspire config set配置的 break-glass 键:

效果
disablePlaywrightCliPackageValidation设为"true"时,跳过所有 Sigstore、provenance 与完整性检查。用于调试 npm 服务问题
playwrightCliVersion设置后覆盖版本范围,固定到指定的精确版本

源码印证:常量DisablePackageValidationKeyVersionOverrideKey定义于 PlaywrightCliInstaller.cs。验证禁用分支(L182-L191)会记录LogWarning,明确提示该配置只应用于调试 npm 服务问题。注意两个键的语义差异:

  • disablePlaywrightCliPackageValidation不会绕过Step 1 的版本解析与 Step 2 的已安装检查,仅跳过 Step 4 的验证(下载的 tarball 直接进入全局安装);
  • playwrightCliVersion只是把 Step 1 的解析范围从>=0.1.3收紧到精确版本,验证流程照常执行——它不是绕过安全机制的通道。

八、未来改进方向

设计文档列出了明确的演进计划:

  1. 内部 attestation 镜像(Internal attestation mirror)——把 npm 的 DSSE attestation 响应保留在内部服务中,使 provenance 验证不再依赖公共 registry 请求。当前 Azure Artifacts 只镜像了 tarball 与 SHA-1 元数据,省略了 SHA-512 完整性值与 attestation bundle;而 Rekor 也无法替代该响应,因为它只存储签名 envelope 与 payload 哈希,而非 DSSE payload 本身。

九、延伸阅读

想从源码层面进一步验证本文的每个论断,可以按以下路径深入:

  • 设计文档:docs/specs/safe-npm-tool-install.md
  • 安装编排主入口:PlaywrightCliInstaller.cs
  • Sigstore / SLSA 验证实现:SigstoreNpmProvenanceChecker.cs
  • 验证门禁与结果类型定义:INpmProvenanceChecker.cs
  • npm 命令执行层(隔离目录、--registry--ignore-scripts):NpmRunner.cs
  • 技能安装位置清单:SkillLocation.cs
  • 触发入口(aspire agent initPhase 4):AgentInitCommand.cs
  • 该机制的 CLI 测试:Aspire.Cli.Tests

这套"硬编码预期值 + 公共注册表解析 + 本地摘要锁定 + Sigstore 密码学验证 + 全字段来源比对"的设计,是一个可复用的安全安装范式:任何需要以用户完整权限安装第三方可执行组件的项目,都可以借鉴它把供应链风险压缩到"信任锚点本身被攻破"这一最小边界。

【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询