2025年的 Crate 安全:工具与技术 (RustConf China 2025系列精选)
本内容是对 RustConf Chian 2025系列演讲中 2025年的 Crate 安全:工具与技术 内容的翻译与整理。推荐点击链接观看原视频。
演讲嘉宾 Adam Harvey, Rust基金会安全软件开发者
好的,谢谢。大家好,我是 Adam。我大概会待在这边,尽量不挡住屏幕。今天我要跟大家聊聊供应链安全。我在加拿大工作。
我在 Rust 基金会担任软件开发工程师,主要专注于生态系统安全。实际工作中,主要是尝试在 crates 里发现恶意软件,并协助处理一些事件,比如昨晚有些人可能看到的,有人试图冒充 crates.io 来窃取大家的 GitHub 令牌。那挺“有趣”的,导致我比预期更晚才睡。我同时也是 Rust crates.io 团队的成员。我应该说明一下,尽管我今天穿着这件 T 恤,我并不是以 Rust 基金会的官方身份发言。但如果我说了什么很聪明的话,那肯定就是 Rust 基金会的官方立场。
我们会比较快地过一遍,因为这是个相对简短的演讲。我会覆盖两个大的方面:第一,整体谈谈供应链安全,以及我所知为改进供应链正在进行的工作;第二,基于这些,介绍一些你可以实际使用的工具,帮助提升你自身供应链安全,改善你所依赖的依赖项的安全性。换句话说,演讲的前半段我会把你们吓一吓,后半段我会告诉你们其实一切都会好起来的。
供应链安全的基本概念并不难。你所使用的一切,尤其在 Rust 这样的语言里,都会使用到其他东西,而那些东西又依赖别的东西;你构建的东西要安全,整条链上的东西都需要安全。实际上,几乎不可能在实践中完全避免这种链式依赖。即便你在做嵌入式系统开发,就像 James 刚刚讲的那样,即使你不使用标准库,你仍然要依赖编译器,可能还要依赖操作系统,而这些同样需要是安全的。
cargo tree -p serde
举个例子,这是一个简单的 crate:Serde,大多数人应该都知道。即便是 Serde 也有 6 个依赖,而且我很确定 David Tolnay 已经尽力把依赖数降到最少了。这意味着,当我们使用 Serde 时,并不是只拉一个 crate,而是拉进来加总起来大概六七个之类的。这会很快累加,因为有些东西比另一些更复杂。
比如这个文本编辑器 Zed,有些人可能听说过,它完全用 Rust 编写。Zed 直接依赖(屏幕上有)164 个 crate,但当你查看整个依赖树时,会拉进来 1115 个依赖。这就意味着有 1115 个 crate 必须是安全的。我就不在这里展示 cargo tree 的输出了。我尝试过把它画成图,但效果并不好。重点是:依赖真的很多。
即便是更小的项目,比如 BAT(一个用 Rust 写的更好的 cat 替代品),也有 30 个直接依赖和 119 个传递依赖。其中很多是我在工作中常见的,比如做命令行解析几乎都会用到 clap。我的工作经常是写一些一次性分析用的小工具。我会说,这些项目平均都会拉进 80 到 100 个依赖。这还不包括任何异步相关的内容,比如 Tokio 等等。总之,东西很多。
我在这里提出三个判断。
第一,你引入了大量你不控制的代码。我在概括。就 Serde 而言,David Tolnay (译者注: 知名Rust程序员, syn 和 quote等库的作者)可能确实掌控了它所有的依赖,但如果你引入的是 clap 和其他东西,那就未必了。
更糟的是,你没有读过这些代码。我肯定没有。再拿 clap 举例,它大约有 9.6 万行 Rust 代码。我见过 Ed Page (译者注: clap的作者) 几次,我相信他写的是好代码,但我不会假装说我对它进行过详细的审查。我大概也就为了弄明白几个边角问题,打开过一两次代码库。
就像在座的大多数人一样,我并不是纯粹为乐趣写 Rust。写 Rust 是很有趣,但我写它是因为工作,而且有截止日期。这意味着我没有时间深入 119 个依赖并逐个审查所有代码库。现实不是那样运作的。
为什么这很重要?我不会深入回顾供应链安全攻击的历史。过去几年我们见到过各种例子,从地下室里的孩子到可能是国家级的攻击者,以及介于两者之间的各种人,都在尝试入侵敏感系统。去年(最有名的)就是 XZ 的事件。我不打算重述历史,你可以自己去搜,我猜大多数人现在已经听说过了。
另一个例子是“打错包名”(typosquatting):Python 里有个常用包叫 Fabric,有人搞了一个叫 Fabrice 的包,末尾多了一个 e。这在一些欧洲国家是常见名字,他们的企图就是让人打错字。有人可能因为打 Fabric 太多次了,装错了包。结果他们的 AWS 密钥就被发给了写这个包的人,后续大概就发生了不好的事。类似的东西很多。我举这个例子,是因为等会儿我会给大家看一些 Rust 恶意软件的例子,但总体来说,形式多种多样。
稍微有点好消息的是,这是一类大公司非常关心的问题,因为他们需要自己的代码是安全的。所以有不少项目(包括我自己的岗位)是在被资助的前提下开展,目的就是发现这些问题并进行防御。现在有相当多的工作在推进这些缓解措施。几个月前一个已经上线的东西叫“Trusted Publishing”(可信发布),在 crates.io 上。感谢资助该工作的 Alpha Omega (译者注: 2022 年 2 月成立的一个项目, “其使命是通过促进最关键的开源软件项目和生态系统的可持续安全改进来保护社会。该项目旨在构建一个关键开源项目安全、安全漏洞被快速发现和修复的世界” )。它允许你把整个 crate 的发布过程完全放在 CI 中进行。目前只支持 GitHub Actions,但计划将来支持其他平台。当然,以我作为 crates.io 团队成员的身份来说,我们非常欢迎外部贡献。
它的意义在于:你在 CI 中使用短期有效的临时凭据发布,从而消除一种风险---
一旦有人拿到了密码或 API Key 就能永久使用。如果 API Key 只有效 15 分钟,且只存在于 CI 系统中,那么攻击者能够利用的窗口就非常小了。
还有一个在进行中的项目,是使用 TUF 这套可信链来为 crates 和 Rust 发行版做签名。共同作者 Josh Triplet 今天也在现场,你可以找他聊聊。这会解锁很多事。短期内,一个关键能力是:可以把 crates 和 Rust 发行版安全地镜像到那些不能不受限访问互联网的环境里。长期看,它也将为 crate 作者发布带有证明(attestation)的 crate 提供基础组件,用以声明“这确实是我打算发布的东西”,并建立信任链。
这个领域有很多机构在活跃。我在幻灯里放了几个:前面提到的 Alpha Omega,他们对多个生态(包括我们)提供大量安全工作的资助;Rust 基金会本身也覆盖了一些工作;OpenSSF 是 Linux 基金会的一个子项目,也提供资助并作为很多工作的公共协作平台;Ecosystems 也在做很多相关分析。以上是理论部分。
接下来谈谈更实际的:我们能做什么?有一个已提到的点是:如果你在发布 crates,可以用 Trusted Publishing 改善你的发布流程安全性。正如前面说的,“把依赖的每一行代码都读一遍”的朴素方法在实践中大概率行不通。除非你读得飞快而且比我更有时间,但大概也不现实。既然做不到,那我们还有哪些数据和工具,能帮助我们在往 cargo.toml 添加依赖时做出更明智的决定?
当然,你也可以选择不依赖外部东西。这也是一种有效选择。我自己也常常犯这个毛病:很容易就“cargo add 一个东西”,因为更快更省事。所以有时候我们应该退一步,想想这是否真的是应该做的。不过,当我回想为什么用 Rust、为什么开始用 Rust,标准库和 crate 生态是很重要的原因之一,因为我不想再自己从零实现一个哈希表之类的。需要记住这一点。但在实践中,我们大多数时候还是会加依赖。如果决定要加依赖,那我们该看什么?遗憾的是,没有一个“这东西绝对安全”的权威指标可以指给你看。但有一些线索,不过其中一些可能会误导。
比如,我在 crates.io 上搜了 “async”,按“历史总下载量”排序。因为 crates.io 的搜索引擎不算好,前两个结果其实没太大用。但看受欢迎程度也还行,Tokio 出现了,如果你做异步,初步看这大概就是你想要的。但下载量可以被操纵。攻击者可以用他们从前面那个“打错包名”里窃取到的 AWS 密钥搭服务器刷下载量。因此这是个很可能误导的信号。不是完全没用,但不是强信号。
我们还能看其他东西。有一些努力在尝试对 Rust 生态里的 crate 做分类,并标注“推荐、广泛使用、高质量”等。这是一个叫 blessed.rs (译者注: Rust 生态系统的非官方指南, 愤懑别类推荐了很多Rust生态的库) 的站点,网址就是那个。我挺喜欢。还有别的,比如 awesome Rust,还有一些更聚焦的清单,如 async、embedded 等。这些都可能是很有用的工具,但它们的可信度取决于来源。
一旦你决定把某个 crate 加进依赖,你当然也需要知道它依赖了什么、它的依赖又依赖了什么。一种方式是生成 SBOM(软件物料清单)。有多种工具,我不打算展开理论。cargo 本身也能告诉你会拉进来什么。有时候这就够你开始梳理了。比如,我们可以拿 cargo tree
的输出,然后统计在该项目依赖图中出现次数超过一次的常见依赖。
结果是:proc-macro2 出现 30 次,serde、quote、syn、tokio 等。这能让你大致知道:如果我要评估安全性,优先看这些“高频”依赖也许更有意义。
当你识别出一些想要仔细看的 crate 后,你需要决定什么对你重要,以及如何分析。接下来是我在看依赖时关注的点,这不是放之四海而皆准的标准,很多人可能会不同意其中一些,这没关系。重点在于方法要系统、一致。我会逐一展开。总体上,我关注:代码质量、安全历史、开发实践、可持续性。
先看代码质量。根本上说,没有什么能替代真正读代码、做评审。但这需要大量时间。那么我们还有什么选择?我觉得“分布式代码评审平台”的理念很有意思。想法是:别人也有同样的问题。有人在使用某个依赖之前可能会审查它,那么这份评审结果就可以被以一种形式发布出来,供其他人参考。你可以决定你有多信任这份评审,但至少它提供了额外数据,我们能“站在别人的肩膀上”。在 Rust 生态里,我知道两个这样的工具:cargo-vet
和 cargo-crev
,可能还有我不知道的。实践上 cargo-vet
的采用更广一些,我主要以它为例。
基本思路是:你做完代码审查,就把结果发布出来。这是 cargo-vet
的格式,一个很简单的 TOML 格式。比如这是来自 Google 的源。每个评审主体(通常是一家公司)发布自己的评审。例子里是对 ahash crate 的审计,版本 0.8.3。你可以指定评审标准,cargo-vet 内置了几个,比如 safe-to-run(顾名思义)以及 Google 自己的“does-not-implement-crypto”。你可以据此得出:“有人审过 ahash 0.8.3,认为安全可运行”。
你对它的信任程度取决于你对 Google 做这件事的信任程度。
好在 cargo-vet 允许你按来源做信任决策。
cargo-vet 还带一个命令行工具,我今天没有时间演示,但挺好用,建议去看看。这些数据也会在 lib.rs 上展示(它是 crates.io 的替代前端)。比如对 Serde,当时最新的 1.0.219 得到了“safe-to-deploy、does-not-implement-crypto、UB risk 2(Google 对“低不安全破坏风险”的表达)”等标注。这有帮助,至少可以让你决定:也许这个 crate 不需要逐行审查。
当然,如果你决定逐行审查,你得决定审什么。我个人首先看这些:任何 unsafe 的使用、build.rs 脚本、过程宏(proc-macros)。对我来说,这些地方最可能隐藏恶意内容。尤其是 unsafe,很微妙;这方面我也会尽量依赖他人的评审结果。
但有个坑:如果你要审 Rust 代码,前述的 build 脚本和过程宏会在你打开它时运行(比如在使用 Rust Analyzer 的编辑器或 IDE 里)。这让安全审计变得困难:因为你一方面可能需要一定程度的"代码智能"功能(如语法分析、提示等),但又不希望如果某个 build 脚本被塞了恶意代码,你的机器就被攻陷。有一些折衷方案:VS Code 可以用受限模式打开,但那样你无法使用"代码智能";
你可以在 docs.rs 上看源码,这其实经常很有用,因为它展示的是 crate 包里实际发布的内容,而不是仓库里可能不同步的内容;你也可以去看仓库,但它不一定与发布包匹配。lib.rs 会告诉你仓库和发布包是否匹配。我们在 Rust 基金会内部也有这方面的跟踪,我很惭愧的是过去一年没来得及把它做成公开看板,但我会的,我保证。
这是 diff.rs,它展示两个 crate 版本之间的差异。比如 Serde 1.0.216 与 1.0.217 的差异。如果你之前已经做过某个版本的审查,这就提供了一个只看“改了什么”的方式。GitHub 也能看 diff,但依然未必和发布包匹配。
第二,安全。Rust 生态有 RustSec,这是 Rust 和 crates 生态已知安全通告的数据库。由 Rust 项目下的 Secure Code 工作组维护。它能给你任何有安全通告的 crate 的安全历史。比如我拿 Hyper 举例(抱歉了 Hyper)。最近一条在 2022 年,还有 2021 年的。点进去之后,内容和你在 CVE Advisories 里熟悉的内容差不多。有受影响的版本。可能最重要的是,它告诉你“不受影响的版本”。所以如果你的依赖链里没有低于 hyper 0.14.12 的版本,那么至少这个问题你是安全的。
RustSec 还提供了一些很好用的工具来消费这些数据。Secure Code 工作组自己有 cargo-audit;还有第三方的 cargo-deny,功能相似但更多。我非常强烈建议你使用其中一个工具(可能 cargo-deny 更好,因为它能做得更多),把它放进 CI。这样当你更新依赖时,它会立刻告诉你是否有已知漏洞、有哪些许可证(如果你关心合规)、依赖从哪里下载等等。如果今天只能带走一件事,那就是:如果你在做 Rust 项目,把这些工具之一放进 CI。
第三,更加模糊的“开发实践”。从项目外部判断一个项目的开发实践真的很难,最好的方法是亲自参与。
当然也有一些指标可看。OpenSSF 有“安全记分卡”(Security Scorecards)。我不太相信用一个生硬的分数去量化一个项目的健康程度和安全实践,但我们可以看看它测什么,看看能不能“择其善者而从之”。OpenSSF 自己不发布记分卡,但 Google 有 deps.dev,基本上提供了这些数据,并且覆盖所有 Rust crates。我挑了一个流行 crate(不告诉你是哪个),看了它在 2025-02-10 的记分卡,综合得分 5.9/10。正如我说的,我不太在意总分,但我们看下分类:Dangerous Workflow(危险工作流)一项显示该仓库未检测到危险的 GitHub Actions 工作流,这很好,10/10;同时它被认为是“活跃维护”的,这也很好。
但你得深挖一下才能判断是否真有价值。比如“Code Review”这一项写着 0/10,“最近 22 个 PR 中仅 2 个被审查”,听起来很糟。他们在干嘛?我去项目看了下,原来最近一个月里 22 个 PR 有 20 个是作者自己提的。没人可以审,所以没被审这事可能可以接受。分数写 0,确实拉低了总分,但可能并不重要。再比如其中有一项似乎只是看 README 里有没有 OpenSSF 徽章,它就占了 10% 的分数。这也许不怎么有用。所以你不能因为 5.9 看起来不高就武断地下结论,因为这些分项的意义并不等价。
最后我关注可持续性。如果你能识别出关键依赖,评估它们的可持续性至关重要。我有一个半小时的专门演讲来讲这个。我刚看到“还剩 5 分钟”的提示,就不试图在剩下时间塞满整个半小时版本了。基本观点是:在我看来,开源维护者如果得到支持与帮助,通常会写出更好的代码。贡献不一定非得是钱,尽管我认识的大多数维护者如果开了 GitHub Sponsors 或其他渠道,都会很乐意接受资助。你也可以在文档、代码、日常维护上帮忙。但关键是:最好的做法是去问他们需要什么。有些维护者可能不需要任何帮助,这完全合理,但很多人是需要的。知道你能如何帮忙很关键。
总结一下,安全供应链的代价是持续的警惕。有许多参与者,包括 Rust 基金会、Alpha Omega、OpenSSF 等,都在关注整个生态(不止 Rust,也包括 Python 等),努力让事情尽可能安全。但到某个节点,你也需要审视自己的依赖,确保自己在引入、使用以及更新依赖时做出好的选择。希望这次演讲至少介绍了一个你可以用上的工具,帮助你在此过程中做出更好的决定。非常感谢你们的时间,也感谢邀请我来这里。