软件供应链安全 (SLSA):在 Go 项目中自动化生成并签署 SBOM 的标准流程
在当今瞬息万变的软件开发环境中,软件供应链的安全性已成为一个不可忽视的核心议题。从 SolarWinds 到 Log4Shell,一系列高影响力事件清晰地揭示了现代软件依赖关系网络的脆弱性。一个微小的漏洞或恶意篡改,潜藏在供应链深处,都可能对整个系统乃至国家安全造成灾难性影响。因此,建立一套透明、可验证、防篡改的机制,以确保软件从源代码到部署的整个生命周期都值得信赖,已成为业界共识。
SLSA (Supply-chain Levels for Software Artifacts),即软件工件的供应链级别,正是在这一背景下应运而生,它是一套端到端的安全框架,旨在提高软件供应链的完整性,并防范各种形式的篡改。与此同时,软件物料清单 (SBOM – Software Bill of Materials) 则作为构建透明度的基石,提供了软件组件的全面列表,使得漏洞追踪和风险管理成为可能。
本次讲座将深入探讨如何在 Go 项目中自动化生成并签署 SBOM,并将其无缝集成到 CI/CD 流程中,从而实践 SLSA 框架所倡导的安全原则。我们将重点关注实际操作、具体工具和代码示例,旨在为开发者和安全工程师提供一套可行的、高效率的解决方案。
1. 软件供应链安全:迫切的需求与挑战
现代软件开发高度依赖于开源组件、第三方库和自动化工具。一个典型的应用程序可能包含数百甚至数千个直接或间接的依赖项。这种便利性在加速开发的同时,也引入了显著的安全风险。
1.1 软件供应链攻击的类型
软件供应链攻击并非单一形式,而是涵盖了从代码编写到部署的多个阶段:
- 上游组件投毒 (Upstream Component Poisoning):攻击者向流行的开源库中注入恶意代码,当其他项目引入这些库时,恶意代码便随之传播。
- 构建系统篡改 (Build System Tampering):攻击者破坏构建服务器、CI/CD 管道或构建脚本,在编译过程中植入后门或修改二进制文件。
- 依赖混淆 (Dependency Confusion):在私有和公共包管理器中存在同名包时,攻击者发布恶意公共包,诱导构建系统下载并使用恶意版本。
- 代码仓库篡改 (Code Repository Tampering):直接修改源代码仓库中的代码,或通过钓鱼等方式获取开发者凭证进行修改。
- 分发渠道劫持 (Distribution Channel Hijacking):攻击者入侵软件分发服务器,替换合法的安装包为恶意版本。
- 签名密钥泄露 (Signing Key Compromise):如果用于签署软件工件的密钥被盗,攻击者可以签署恶意软件,使其看起来合法。
1.2 透明度与可验证性的缺失
面对这些威胁,传统的安全措施往往力不从心。问题的核心在于,我们通常缺乏对软件工件来源、构建过程和其中包含组件的全面了解。这种透明度的缺失导致:
- 难以追踪漏洞:当一个流行的库(如 Log4j)被曝出严重漏洞时,企业需要迅速识别所有受影响的应用程序。如果没有精确的组件清单,这项任务将异常艰难且耗时。
- 无法验证完整性:我们如何确信下载的二进制文件与源代码完全对应,并且在传输过程中未被篡改?
- 缺乏信任根:我们如何信任一个从未提供其构建过程可审计性的软件供应商?
为了解决这些问题,我们需要一套标准化的方法来提供软件的“出生证明”和“履历”,这正是 SLSA 和 SBOM 共同努力的方向。
2. SLSA 框架:构建可信赖的软件供应链
SLSA (Supply-chain Levels for Software Artifacts) 是由 Google 发起并由社区驱动的一项倡议,旨在为软件供应链安全提供一套分级的、可审计的框架。它的目标是使开发人员和消费者能够自信地评估软件工件的安全性。
2.1 SLSA 的核心目标
- 防止篡改 (Prevent Tampering):确保软件工件在生成、存储和传输过程中不被未经授权的人员或系统修改。
- 提高完整性 (Improve Integrity):通过加密签名和不可变存储来保证工件的原始状态。
- 实现可审计性 (Enable Auditability):提供详细的、可验证的构建过程记录(即 Provenance),以便追溯软件的来源和构建方式。
2.2 SLSA 级别概述
SLSA 定义了四个逐渐增强的安全级别,每个级别都增加了额外的安全控制和保证。
| SLSA 级别 | 描述 | 核心要求 | 适用场景 |
|---|---|---|---|
| SLSA 1 | 基本来源信息 (Automated Provenance) | 构建脚本自动化,并提供关于构建来源和构建过程的初步信息。 | 适用于任何项目,提供基础透明度。 |
| SLSA 2 | 签名来源信息 (Signed Provenance) | 在 SLSA 1 的基础上,构建过程由版本控制系统管理,并且构建系统会生成并签署构建来源信息。 | 适用于需要验证构建过程未被篡改的项目。 |
| SLSA 3 | 防篡改构建 (Hardened Builds) | 在 SLSA 2 的基础上,要求构建在隔离且短暂的环境中进行,并生成强化的、不可伪造的来源信息。 | 适用于需要高可信度软件,如关键基础设施组件。 |
| SLSA 4 | 双重审查、密封构建 (Two-Person Reviewed, Hermetic Builds) | 最高级别。要求构建过程是完全密封的(Hermetic),所有的输入都已声明且经过双重审查,确保输出的可重现性。 | 适用于对安全性有极致要求的场景,如国家安全、金融系统核心组件。 |
我们的目标是至少达到 SLSA 3,这要求我们能够在一个隔离的环境中自动化构建,并生成可信赖的、签名过的构建来源信息。这正是 SBOM 生成和签名与 CI/CD 流程结合的重点。
2.3 核心概念:Provenance (来源证明)
Provenance 是 SLSA 的核心。它是一份关于软件工件如何被构建的详细记录。这份记录包含了:
- 构建器 (Builder):哪个系统或工具执行了构建。
- 构建步骤 (Build Steps):执行了哪些命令和操作。
- 输入 (Inputs):构建所使用的所有源代码、依赖项和配置文件。
- 输出 (Outputs):构建产生的工件及其哈希值。
- 环境 (Environment):构建发生时的环境配置。
Provenance 必须是机器可读的(通常是 JSON 格式),并且经过加密签名,以防止篡改。这使得消费者可以验证工件的来源是否符合预期,并检查其是否在构建过程中被篡改。
3. SBOM:软件物料清单作为透明度的基石
SBOM (Software Bill of Materials) 是一种正式的、机器可读的、包含软件组件及其依赖项的完整清单。可以将其类比为食品包装上的成分表,它详细列出了软件“产品”的所有“成分”。
3.1 SBOM 的重要性
- 漏洞管理:当发现某个库存在严重漏洞时,SBOM 能够快速识别所有使用该库的应用程序,从而实现精准修补。
- 许可合规性:清晰列出所有组件的许可证,帮助企业遵守开源许可证协议,避免法律风险。
- 供应链风险评估:了解所有依赖项,包括其来源、版本和已知漏洞,有助于评估整个软件供应链的风险状况。
- 安全审计:为安全审计师提供全面信息,以便他们评估软件的安全性。
- 客户信任:向客户提供 SBOM,增强了他们对软件产品透明度和安全性的信任。
3.2 常见的 SBOM 格式
目前,业界主要有两种标准化的 SBOM 格式:
- SPDX (Software Package Data Exchange):由 Linux 基金会主导,是一个 ISO 标准 (ISO/IEC 5962:2021)。它提供了一个灵活的框架来描述软件组件、许可证和安全信息。
- CycloneDX:由 OWASP (Open Web Application Security Project) 社区主导,旨在成为轻量级、全面的 SBOM 标准,特别适用于自动化和应用程序安全用例。
两者都支持多种序列化格式,如 JSON、XML 等。在实践中,CycloneDX 因其简洁性和对自动化工具的友好性而越来越受欢迎。
3.3 SBOM 与 SLSA 的关系
SBOM 是实现 SLSA 目标的关键组成部分。
- 输入透明度:SLSA 的 Provenance 记录了构建的输入。SBOM 提供了这些输入中所有第三方组件的详细列表。
- 可验证性:SLSA 强调签名 Provenance。同样,签署 SBOM 确保了其内容的完整性和真实性。
- 漏洞管理:SLSA 旨在确保工件未被篡改,但它不直接解决组件漏洞。SBOM 提供了识别和管理这些漏洞所需的数据。
简而言之,SLSA 提供了关于“软件如何被构建”的信任,而 SBOM 则提供了“软件由什么组成”的透明度。两者结合,才能建立起真正可信赖的软件供应链。
4. 在 Go 项目中自动化生成 SBOM
Go 语言以其简洁、高效和强大的依赖管理(go mod)而闻名。然而,管理和理解 Go 项目的传递性依赖仍然是一个挑战。幸运的是,有一些优秀的工具可以帮助我们自动化生成 Go 项目的 SBOM。
我们将重点介绍 syft,它是一个功能强大且广泛使用的命令行工具,能够从多种源(文件系统、容器镜像、Go 模块等)中提取软件组件和依赖关系,并输出为 SPDX 或 CycloneDX 格式的 SBOM。
4.1 示例 Go 项目
为了演示,我们先创建一个简单的 Go 项目,它会引入一些第三方依赖。
mkdir go-sbom-example
cd go-sbom-example
go mod init github.com/yourusername/go-sbom-example
# 引入一些常见的第三方库
go get github.com/sirupsen/logrus
go get github.com/spf13/cobra
go get github.com/go-chi/chi/v5
现在,go.mod 文件会包含这些直接依赖及其传递依赖。
// main.go
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "go-app",
Short: "A simple Go application with multiple dependencies",
Run: func(cmd *cobra.Command, args []string) {
logrus.Info("Starting Go application...")
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go app with Chi and Logrus!n")
})
logrus.WithField("port", 8080).Info("Server listening on :8080")
http.ListenAndServe(":8080", r)
},
}
func main() {
if err := rootCmd.Execute(); err != nil {
logrus.WithError(err).Fatal("Failed to execute root command")
}
}
构建项目:
go build -o myapp
4.2 安装 Syft
syft 的安装非常简单,可以通过多种方式进行。
使用 Homebrew (macOS/Linux):
brew install syft
使用 Go 安装 (需要 Go 环境):
go install github.com/anchore/syft/cmd/syft@latest
直接下载二进制文件:
从 GitHub Releases 页面下载适合你操作系统的二进制文件,并将其添加到 PATH。
https://github.com/anchore/syft/releases
验证安装:
syft version
4.3 使用 Syft 生成 SBOM
syft 可以分析多种源。对于 Go 项目,我们可以直接让它分析当前的目录、Go 模块缓存或构建好的二进制文件。
分析当前目录下的 Go 模块依赖:
这是最常用的方法,syft 会自动识别 go.mod 和 go.sum 文件。
syft packages dir:. --scope all-dependencies --output cyclonedx-json > sbom.json
参数解释:
packages dir:.:指示syft分析当前目录。--scope all-dependencies:确保包含所有直接和传递依赖。--output cyclonedx-json:指定输出格式为 CycloneDX JSON。你也可以选择spdx-json、cyclonedx-xml等。> sbom.json:将输出重定向到sbom.json文件。
执行上述命令后,sbom.json 文件将包含我们 Go 项目的所有依赖信息。
示例 sbom.json 片段 (部分内容,实际文件会更长):
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:a1b2c3d4-e5f6-7890-1234-567890abcdef",
"version": 1,
"metadata": {
"timestamp": "2023-10-27T10:00:00Z",
"tools": [
{
"vendor": "anchore",
"name": "syft",
"version": "0.90.0"
}
],
"component": {
"type": "application",
"name": "go-sbom-example",
"purl": "pkg:golang/github.com/yourusername/[email protected]"
}
},
"components": [
{
"bom-ref": "pkg:golang/github.com/go-chi/chi/[email protected]?arch=amd64&goarch=amd64&goos=linux&ostype=linux",
"type": "library",
"name": "chi",
"version": "5.0.10",
"purl": "pkg:golang/github.com/go-chi/chi/[email protected]",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"properties": [
{
"name": "syft:package:root",
"value": "false"
}
]
},
{
"bom-ref": "pkg:golang/github.com/sirupsen/[email protected]?arch=amd64&goarch=amd64&goos=linux&ostype=linux",
"type": "library",
"name": "logrus",
"version": "1.9.3",
"purl": "pkg:golang/github.com/sirupsen/[email protected]",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"properties": [
{
"name": "syft:package:root",
"value": "false"
}
]
},
// ... 更多依赖项,包括传递依赖
{
"bom-ref": "pkg:golang/golang.org/x/[email protected]?arch=amd64&goarch=amd64&goos=linux&ostype=linux",
"type": "library",
"name": "sys",
"version": "0.12.0",
"purl": "pkg:golang/golang.org/x/[email protected]",
"licenses": [
{
"license": {
"id": "BSD-3-Clause"
}
}
],
"properties": [
{
"name": "syft:package:root",
"value": "false"
}
]
}
],
"dependencies": [
{
"ref": "pkg:golang/github.com/yourusername/[email protected]",
"dependsOn": [
"pkg:golang/github.com/sirupsen/[email protected]",
"pkg:golang/github.com/spf13/[email protected]",
"pkg:golang/github.com/go-chi/chi/[email protected]"
]
},
// ... 更多依赖关系
]
}
syft 能够深入分析 Go 模块的结构,正确识别直接和传递依赖,并尝试提取许可证信息。这些信息对于后续的漏洞扫描和合规性检查至关重要。
4.4 SBOM 的后续利用
生成 SBOM 后,可以将其用于:
-
漏洞扫描:使用
grype(与syft同属 Anchore 公司) 等工具扫描 SBOM,发现已知漏洞。# 安装 grype brew install grype # 或 go install github.com/anchore/grype/cmd/grype@latest # 扫描 sbom.json grype sbom.jsongrype会根据 SBOM 中列出的组件,对照各种漏洞数据库(如 NVD、Go Vuln DB)来报告潜在的安全问题。 - 上传到 SBOM 管理平台:如 OWASP Dependency-Track,用于持续监控和管理项目及其依赖的风险。
5. 签署 SBOM:确保真实性与完整性
生成 SBOM 只是第一步。为了满足 SLSA 级别 2 及更高的要求,并确保 SBOM 在传输和存储过程中不被篡改,我们必须对其进行数字签名。数字签名提供了两个关键保证:
- 真实性 (Authenticity):验证 SBOM 确实是由声称的实体(例如,你的 CI/CD 系统或开发者)生成的。
- 完整性 (Integrity):确保 SBOM 的内容自签名以来未被修改。
我们将使用 cosign,这是一个来自 Sigstore 项目的工具,专门用于签署、验证和存储软件工件(包括容器镜像、二进制文件和 SBOM)的签名。cosign 的设计目标是简化软件签名过程,并使其更易于自动化集成。
5.1 安装 Cosign
cosign 的安装也很简单。
使用 Homebrew (macOS/Linux):
brew install cosign
使用 Go 安装:
go install github.com/sigstore/cosign/cmd/cosign@latest
直接下载二进制文件:
从 GitHub Releases 页面下载适合你操作系统的二进制文件,并将其添加到 PATH。
https://github.com/sigstore/cosign/releases
验证安装:
cosign version
5.2 Cosign 的签名机制
cosign 支持多种签名机制:
- 本地密钥对 (Local Key Pair):生成一个公钥/私钥对,私钥用于签名,公钥用于验证。这是最传统的方式。
- 无密钥签名 (Keyless Signing):利用 Sigstore 的公共透明日志 Rekor 和短期证书颁发机构 Fulcio。它通过 OpenID Connect (OIDC) 身份提供者来验证签名者的身份,并颁发一个有效期很短的证书来签署工件。这种方式避免了密钥管理和分发的问题,是 SLSA 推荐的最佳实践。
我们将先演示本地密钥对签名,然后深入探讨无密钥签名,因为它更适合 CI/CD 环境。
5.3 使用本地密钥对签署 SBOM
生成密钥对:
cosign generate-key-pair
该命令会生成 cosign.key (私钥) 和 cosign.pub (公钥),并要求你设置一个密码来保护私钥。请务必妥善保管私钥和密码!在自动化环境中,这通常通过环境变量或密钥管理服务来提供。
签署 SBOM 文件:
假设我们已经有了 sbom.json。
# 使用私钥签署 sbom.json 文件
# 系统会提示输入私钥密码
cosign sign-blob --key cosign.key sbom.json
执行后,cosign 会生成一个 .sig 文件(例如 sbom.json.sig),其中包含了 SBOM 的数字签名。
验证签名:
为了验证 sbom.json 的完整性和真实性,我们需要公钥。
# 使用公钥验证 sbom.json 和其签名
cosign verify-blob --key cosign.pub --signature sbom.json.sig sbom.json
如果验证成功,你将看到类似 Verified OK 的输出。如果文件被篡改或签名不匹配,验证将失败。
5.4 无密钥签名 (Keyless Signing) 与 Sigstore
无密钥签名是 cosign 的一个强大功能,它极大地简化了密钥管理,特别适用于 CI/CD 自动化。它依赖于 Sigstore 项目的核心组件:
- Fulcio:一个短期证书颁发机构 (Certificate Authority),它通过 OIDC 身份提供者(如 GitHub、Google、Microsoft)验证用户的身份,并颁发一个短寿命的 X.509 证书。
- Rekor:一个公开的、防篡改的透明日志。所有由 Fulcio 签发的证书和签名的元数据都会被记录到 Rekor 中。这提供了不可否认的证据,并允许任何人审计签名历史。
无密钥签名的流程:
- 身份验证:
cosign通过 OIDC 与身份提供者(例如 GitHub Actions)进行交互,获取一个身份令牌。 - 证书请求:
cosign使用该身份令牌向 Fulcio 请求一个短期签名证书。Fulcio 验证令牌并颁发证书。 - 签名:
cosign使用私有密钥(在本地临时生成并立即丢弃)对 SBOM 进行签名,并使用 Fulcio 颁发的证书来证明签名者的身份。 - 日志记录:签名及其相关的证书和 OIDC 身份信息被上传到 Rekor 透明日志中。
使用无密钥签名签署 SBOM:
# 这会在后台与 Fulcio 和 Rekor 交互
# 如果在本地运行,会提示打开浏览器进行 OIDC 认证
cosign sign-blob sbom.json
签名成功后,cosign 不会生成 .sig 文件。相反,签名和证书会被存储在 Rekor 日志中。
验证无密钥签名:
验证时,cosign 会从 Rekor 日志中查找与 sbom.json 相关的签名和证书,并验证其有效性。
cosign verify-blob sbom.json
cosign 会自动从 Rekor 拉取签名和证书,并验证它们。如果验证成功,它还会显示签名者的 OIDC 身份信息,例如 GitHub 用户名或工作流 ID。
无密钥签名是实现 SLSA 级别 3 甚至 4 的关键,因为它提供了一个去中心化、可审计且无需管理长期私钥的签名机制。
6. 集成到 CI/CD 管道:实现 SLSA L3/L4
将 SBOM 生成和签名集成到 CI/CD 管道是自动化软件供应链安全的关键一步。这将确保每次构建都生成并签署 SBOM,从而提供持续的透明度和可验证性。我们将以 GitHub Actions 为例,演示一个完整的自动化流程。
6.1 GitHub Actions 中的 SLSA L3 实践
为了达到 SLSA L3,我们的 GitHub Actions 工作流需要满足以下条件:
- 隔离的构建环境:GitHub Actions 提供的虚拟机是短暂且隔离的,这满足了 SLSA 对“Ephemeral Environment”的要求。
- 不可伪造的 Provenance:使用
slsa-github-generator等工具生成符合 SLSA 规范的构建 Provenance。 - 自动化 SBOM 生成与签名:在构建过程中自动生成 Go 项目的 SBOM,并使用
cosign进行无密钥签名。 - 工件存储:将构建好的 Go 二进制文件、SBOM 和签名上传到安全的存储位置,例如 OCI 注册表 (GitHub Container Registry, Docker Hub, GCR 等)。
6.2 完整的 GitHub Actions 工作流示例
以下是一个 GitHub Actions 工作流 (.github/workflows/build-and-sign.yml) 示例,它将构建一个 Go 应用程序,生成并签署 SBOM,并将所有工件推送到 GitHub Container Registry (GHCR)。
name: Go Build, SBOM, and SLSA Signing
on:
push:
branches:
- main
workflow_dispatch: # 允许手动触发
permissions: read-all # 默认权限,最小化原则
env:
GO_VERSION: '1.21'
APP_NAME: 'go-sbom-example' # Go 应用名称
SBOM_FILE: 'sbom.json' # SBOM 文件名
# OCI 注册表路径,用于存储应用二进制、SBOM 和签名
# 例如:ghcr.io/yourusername/go-sbom-example
OCI_IMAGE_PATH: 'ghcr.io/${{ github.repository }}'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # 允许写入代码仓库 (用于上传构建工件,如果需要)
id-token: write # 用于 Sigstore OIDC 身份验证
packages: write # 允许写入 GitHub Packages (GHCR)
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Build Go application
run: |
go mod tidy
go build -o ${{ env.APP_NAME }} .
- name: Install Syft
uses: anchore/[email protected]
with:
version: 'latest' # 或指定版本如 '0.90.0'
- name: Generate SBOM (CycloneDX JSON)
run: |
syft packages dir:. --scope all-dependencies --output cyclonedx-json > ${{ env.SBOM_FILE }}
echo "Generated SBOM:"
cat ${{ env.SBOM_FILE }} | head -n 20 # 打印前20行查看
shell: bash
- name: Install Cosign
uses: sigstore/[email protected]
with:
cosign-release: 'v2.2.3' # 推荐指定版本
- name: Sign SBOM with Cosign (Keyless)
run: |
# 确保环境变量 COSIGN_EXPERIMENTAL 已设置,以启用无密钥签名
export COSIGN_EXPERIMENTAL=true
# 签署 SBOM 文件,签名和证书将上传到 Sigstore Rekor
# 这里我们不指定 --key,Cosign 会自动使用 OIDC 进行无密钥签名
cosign sign-blob
--output-signature ${{ env.SBOM_FILE }}.sig
--output-certificate ${{ env.SBOM_FILE }}.crt
${{ env.SBOM_FILE }}
env:
# GitHub Actions 提供了 OIDC 令牌,Cosign 会自动使用它
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 通常不需要显式传递,但为了清晰可以提及
# 设置 COSIGN_REPOSITORY 以指定签名存储位置,默认为 Rekor 日志
# 如果想将签名存储在 OCI 注册表中,可以使用类似下面的配置:
# COSIGN_REPOSITORY: ${{ env.OCI_IMAGE_PATH }}
shell: bash
- name: Upload SBOM to GitHub Container Registry (GHCR)
# 将 SBOM 作为 OCI 工件上传,并附带签名和证书
run: |
# 登录到 GHCR
echo "${{ secrets.GITHUB_TOKEN }}" | cosign login ghcr.io -u ${{ github.actor }} --password-stdin
# 将 SBOM 推送为 OCI 工件
# Tag 格式:ghcr.io/owner/repo/sbom:latest (或使用 commit SHA)
# 使用 `cosign attach` 可以将签名和证书附加到 SBOM 工件上
# 或者直接上传 SBOM 文件,然后将签名和证书作为单独的工件上传
# 为了简化,这里先将 SBOM 作为文件上传到 artifact,然后手动推送
# 推送 SBOM 文件本身作为一个 OCI 工件
# 使用 `oras push` 或 `cosign attach`
# 针对 SBOM,更常见的是直接将其作为构建 Artifact 上传,或存储在 S3/GCS
# 但为了演示 OCI 存储和签名,我们把它推送到 GHCR,但用特定的 media type
# 创建一个唯一的 tag
SBOM_TAG_SUFFIX=${{ github.sha }}
# SBOM_OCI_REF=${{ env.OCI_IMAGE_PATH }}/sbom:${SBOM_TAG_SUFFIX}
# 仅上传 SBOM 文件作为 GitHub Action Artifact
# 这个更常用,因为消费者下载应用后,可以单独下载 SBOM
echo "Uploading SBOM as GitHub Actions Artifact..."
- name: Upload SBOM and Signature as GitHub Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.APP_NAME }}-sbom-and-signature
path: |
${{ env.SBOM_FILE }}
${{ env.SBOM_FILE }}.sig
${{ env.SBOM_FILE }}.crt
${{ env.APP_NAME }} # 上传应用二进制文件
# --- SLSA Provenance Generation ---
# 这一步将生成关于整个 GitHub Actions 构建过程的 SLSA Provenance
# 这是一个独立的步骤,用于证明构建本身是可信的
- name: Generate SLSA Provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
# SLSA Provenance 的输出路径,通常是一个 JSON 文件
provenance-name: ${{ env.APP_NAME }}.slsa.json
# 如果你的 Go 项目有多个模块,可能需要指定
# module: ./
# Go 版本
go-version: ${{ env.GO_VERSION }}
# Go Build 命令,确保它与实际构建命令一致
go-build-command: go build -o ${{ env.APP_NAME }} .
# 注意:generator_generic_go.yml 已经包含了构建和签名步骤
# 如果你已经手动构建和签名,可能需要调整或使用更通用的 generator_generic.yml
# 这里为了演示,我们假设 generator_generic_go.yml 负责所有
# 实际项目中,你可能需要更细粒度的控制,或者使用 generator_generic.yml
# 并将 SBOM 生成和签名作为单独的步骤添加到 Provenance 中。
# 对于此示例,我们假设 generator_generic_go.yml 仅生成 Provenance,不重复构建
# 实际使用时,你需要根据 generator 的具体功能来调整
# 为了避免重复构建,通常会使用 `generator_generic.yml` 配合 `inputs` 来指定已构建的工件
# 但如果目标是生成一个 Go 应用的 Provenance,`generator_generic_go.yml` 是推荐的。
# 为了简化和避免冲突,我们暂时只用它来生成 Go 应用的 Provenance。
# 实际上,`generator_generic_go.yml` 会自己构建 Go 应用并生成 Provenance。
# 所以,上面手动构建 Go 应用的步骤,如果使用此 generator,可以移除。
# 修正:slsa-github-generator 的 `generator_generic_go.yml` 会自己处理 Go 构建和签名。
# 所以,为了遵循其最佳实践,我们应该让它来驱动整个过程,而不是分开手动执行。
# 下面是修改后的 SLSA Provenance 部分的思路,需要重新设计整个 Job。
# 重新设计的 Job 结构:
# 1. Checkout
# 2. 调用 slsa-github-generator 的 `generator_generic_go.yml`,让它负责构建、签名和生成 Provenance。
# 3. 在 generator 完成后,获取其输出的 SBOM 和 Provenance 文件。
# --- 重新设计的 Job,以 slsa-github-generator 为主导 ---
build_with_slsa:
runs-on: ubuntu-latest
permissions:
contents: write # 允许写入代码仓库 (用于上传构建工件,如果需要)
id-token: write # 用于 Sigstore OIDC 身份验证
packages: write # 允许写入 GitHub Packages (GHCR)
outputs:
# 输出构建的二进制文件和 SBOM 的路径,供后续步骤使用
app_artifact: ${{ steps.build_app.outputs.app_artifact }}
sbom_artifact: ${{ steps.build_app.outputs.sbom_artifact }}
provenance_artifact: ${{ steps.build_app.outputs.provenance_artifact }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate SLSA Provenance and Build Go Application
id: build_app # 为此步骤分配 ID 以便引用其输出
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
# Go 版本
go-version: ${{ env.GO_VERSION }}
# Go Build 命令
go-build-command: go build -o ${{ env.APP_NAME }} .
# 生成 SBOM (true/false)。此 generator 默认会使用 syft 生成 CycloneDX SBOM
# 如果想关闭或使用其他工具,可能需要更通用的 generator_generic.yml
generate-sbom: true
# SBOM 的输出格式,默认为 cyclonedx-json
sbom-format: cyclonedx-json
# SBOM 的文件名称
sbom-file: ${{ env.SBOM_FILE }}
# 工件名称,用于 GitHub Actions artifact
# generator 会自动上传二进制文件、SBOM 和 Provenance
# 所以这里不再需要手动 upload-artifact
artifact-name: ${{ env.APP_NAME }}
# 输出路径可以在 generator 中配置,但通常 generator 会将它们作为 artifact 上传
# 并将路径作为 output 提供。我们需要查看 generator 的文档来获取确切的输出变量名。
# 假设 generator 会输出这些路径
# outputs:
# app_artifact: ${{ steps.build_app.outputs.go-binary-path }} # 这是一个假设的输出变量名
# sbom_artifact: ${{ steps.build_app.outputs.sbom-path }}
# provenance_artifact: ${{ steps.build_app.outputs.provenance-path }}
# 实际上,`slsa-github-generator` 会将所有生成的工件(二进制、SBOM、Provenance)
# 作为 GitHub Actions Artifact 上传,并进行签名。
# 所以,一旦此步骤完成,我们就可以认为工件已经生成、签署并存储在 GitHub Actions Artifacts 中。
# 消费者需要下载这些 Artifacts 进行验证。
# 这一步是可选的,如果 generator 已经自动上传了 Artifact
# 但为了明确,我们可以再次提及下载 Artifact 的方式。
- name: Download Generated Artifacts (for verification purposes within workflow)
uses: actions/download-artifact@v4
with:
name: ${{ env.APP_NAME }} # generator 默认的 artifact 名称
path: ./artifacts # 下载到当前目录下的 artifacts 文件夹
- name: Verify SBOM (Optional, for self-check in CI)
run: |
# 这一步是 CI 内部的自我检查,确保 SBOM 确实生成了
# 消费者会做更严格的验证
ls -l ./artifacts
cat ./artifacts/${{ env.SBOM_FILE }} | head -n 20
# 验证 SBOM 签名 (如果 generator 签名了 SBOM)
# 注意:slsa-github-generator 会对**整个构建过程**生成 Provenance 并签名,
# 也会对**构建产物**(包括 Go 二进制和 SBOM)进行签名,并推送到 Rekor。
# 验证时需要知道工件的 OCI 引用。
# 假设 generator 将 SBOM 推送到了 GHCR,并提供了引用
# 例如:cosign verify --certificate-identity "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_go.yml@refs/heads/main" --certificate-oidc-issuer "https://token.actions.githubusercontent.com" ghcr.io/owner/repo/go-app@sha256:APP_DIGEST
# 验证 SBOM 签名会类似:
# cosign verify --certificate-identity "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_go.yml@refs/heads/main" --certificate-oidc-issuer "https://token.actions.githubusercontent.com" ghcr.io/owner/repo/go-app-sbom:latest
# 由于 generator 封装了签名过程,这里的验证通常在消费者端进行。
# 在 CI 内部进行验证会比较复杂,需要获取 generator 内部生成的 OCI 引用。
echo "SBOM and Provenance generated and signed by SLSA generator."
工作流解释:
permissions:contents: write:允许工作流将文件推送到仓库,虽然我们主要通过 GitHub Artifacts 和 GHCR 进行存储,但某些情况下可能需要此权限。id-token: write:这是进行 Sigstore 无密钥签名的关键。它允许工作流请求一个 OIDC 令牌,用于向 Fulcio 证明其身份。packages: write:允许工作流将工件推送到 GitHub Packages (GHCR)。
Checkout code:拉取 Go 项目的源代码。Generate SLSA Provenance and Build Go Application:这是核心步骤。- 我们使用
slsa-framework/slsa-github-generator/.github/workflows/generator_generic_go.yml这个 Reusable Workflow。 - 这个生成器会:
- 设置 Go 环境。
- 根据
go-build-command构建 Go 应用程序。 - 使用
syft生成 Go 应用程序的 SBOM。 - 使用
cosign对构建的 Go 二进制文件和 SBOM 进行无密钥签名,并将签名上传到 Rekor。 - 生成符合 SLSA 规范的构建 Provenance (通常是
*.attestation或*.slsa.json文件),并对其进行签名。 - 将所有这些工件(Go 二进制、SBOM、Provenance、它们的签名)作为 GitHub Actions Artifact 上传。
- 通过
artifact-name参数,我们可以指定上传的 Artifact 的名称。 - 此步骤的输出会包含生成的工件路径。
- 我们使用
Download Generated Artifacts:此步骤是可选的,仅用于在 CI 内部验证。在实际消费者场景中,用户会直接从 GitHub Actions Artifacts 或 GHCR 下载。Verify SBOM (Optional, for self-check in CI):同样是可选的内部检查。实际的 SLSA 验证流程会在消费者端发生。
slsa-github-generator 的优势:
- 开箱即用:它封装了 SLSA 级别 3 的所有复杂性,包括隔离构建、Provenance 生成、签名等。
- 自动化:通过简单的配置,即可在每次构建时自动生成并签署符合 SLSA 规范的工件。
- 无密钥签名:利用 GitHub Actions 的 OIDC 身份验证,实现了无密钥签名,极大地简化了密钥管理。
现在,每次将代码推送到 main 分支时,GitHub Actions 都会运行此工作流,生成 Go 应用程序,为其生成 SBOM,对两者进行签名,并生成关于整个构建过程的 SLSA Provenance。所有这些工件都将被上传为 GitHub Actions Artifacts。
7. 验证整个链条:从代码到部署的信任
构建、生成 SBOM 并签名只是“生产端”的工作。真正的价值体现在“消费端”:消费者如何验证他们所使用的软件是真实、完整且来自可信来源的?这需要一系列的验证步骤。
7.1 验证流程概述
消费者下载软件工件后,将执行以下验证步骤:
- 验证应用程序二进制文件的 SLSA Provenance:确保应用程序的构建过程符合 SLSA 规范,并且未被篡改。
- 验证 SBOM 的签名:确保 SBOM 文件本身未被篡改,并且是由可信的构建系统或发行方生成的。
- 分析 SBOM:检查 SBOM 中列出的组件是否存在已知的安全漏洞。
7.2 验证 SLSA Provenance
slsa-github-generator 会生成一个 .attestation 或 .slsa.json 文件,其中包含了构建的 Provenance 信息,并且这个文件本身也是经过签名的。
下载工件:
消费者首先需要从 GitHub Actions Artifacts 下载应用程序二进制文件、SBOM 和 Provenance 文件。
# 假设你在本地创建了一个目录并下载了 Artifact
mkdir downloaded-artifacts
# 手动从 GitHub Actions 界面下载 Artifact 到此目录
# 或者使用 GitHub CLI: gh run view <run-id> --artifact <artifact-name>
现在,假设我们有:
myapp(Go 应用程序二进制文件)sbom.json(SBOM 文件)myapp.slsa.json(SLSA Provenance 文件)
使用 slsa-verifier 验证 Provenance:
slsa-verifier 是 SLSA 框架提供的命令行工具,用于验证由 slsa-github-generator 生成的 Provenance。
安装 slsa-verifier:
go install github.com/slsa-framework/slsa-verifier/cli/slsa-verifier@latest
验证步骤:
# 假设你的 Go 模块路径是 github.com/yourusername/go-sbom-example
# 假设你的工作流文件是 .github/workflows/build-and-sign.yml
slsa-verifier verify-artifact
--source-uri "github.com/yourusername/go-sbom-example"
--tag "v1.0.0" # 或者 --branch "main" 或 --versioned-tag "v1.0.0" 等,取决于你的发布策略
--provenance-path "./downloaded-artifacts/myapp.slsa.json"
--artifact-path "./downloaded-artifacts/myapp"
参数解释:
--source-uri:Go 项目的 GitHub 仓库 URI。--tag:如果你的发布是基于 Git Tag 的,则指定 Tag。否则,可能需要指定branch或commit。--provenance-path:SLSA Provenance 文件的本地路径。--artifact-path:要验证的应用程序二进制文件的本地路径。
slsa-verifier 会执行一系列检查:
- 验证 Provenance 文件本身的签名,确保其来自可信的 GitHub Actions 运行。
- 检查 Provenance 中记录的构建步骤、输入和输出是否与期望的一致。
- 验证 Provenance 中记录的应用程序哈希值是否与本地下载的应用程序哈希值匹配。
- 确保 Provenance 是由
slsa-github-generator生成的,并且构建环境是隔离和短暂的(SLSA L3 保证)。
如果所有检查通过,slsa-verifier 将输出 PASSED,表示应用程序二进制文件及其构建过程符合 SLSA 规范。
7.3 验证 SBOM 签名
在前面的 CI/CD 示例中,slsa-github-generator 会自动对 SBOM 进行签名,并将签名信息上传到 Rekor。
验证 SBOM 签名:
# 首先,确保 cosign 已安装
# 然后,使用 cosign 验证 SBOM 文件
# 注意:你需要指定签名者的身份信息,这些信息可以在 Provenance 或 Rekor 日志中找到
# 理想情况下,slsa-github-generator 会在 Provenance 中记录签名信息
# 假设 slsa-github-generator 的 OIDC identity 是固定的
# Issuer: https://token.actions.githubusercontent.com
# Subject (Identity): https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_go.yml@refs/heads/main
cosign verify-blob
--certificate-identity "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_go.yml@refs/heads/main"
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
--insecure-ignore-tlog
./downloaded-artifacts/sbom.json
# 如果签名文件是单独的 .sig,你需要 --signature 参数
# 如果证书是单独的 .crt,你需要 --certificate 参数
# 但对于 keyless 签名,通常 cosign 会自动从 Rekor 查找
注意:--insecure-ignore-tlog 在生产环境中不推荐使用,它会跳过 Rekor 透明日志的验证。在实际场景中,cosign 会自动与 Rekor 交互,确保签名和证书在日志中被记录且未被篡改。完整的验证需要 Rekor。
如果验证成功,cosign 将显示签名者的身份信息,并确认 SBOM 未被篡改。
7.4 分析 SBOM 以发现漏洞
一旦 SBOM 被验证为真实且完整,下一步就是使用漏洞扫描工具对其进行分析。
# 扫描 SBOM 文件
grype sbom:./downloaded-artifacts/sbom.json
grype 会解析 sbom.json 文件,查找其中列出的所有组件及其版本,然后将其与各种公共和私有漏洞数据库进行比对。它会报告任何已知的漏洞,包括 CVE ID、严重性、受影响的版本等。
通过这三个步骤(验证应用程序 Provenance、验证 SBOM 签名、扫描 SBOM),消费者可以建立对软件的高度信任,并有效管理其供应链风险。
8. 高级考量与最佳实践
将 SLSA 和 SBOM 整合到 Go 项目的开发流程中,并非一劳永逸。为了充分发挥其价值并应对不断演变的安全威胁,还需要考虑一些高级实践。
8.1 持续监控与策略执行
- SBOM 持续更新:每次构建都应生成新的 SBOM。依赖项可能会发生变化,新的漏洞也可能被发现。
- 漏洞持续扫描:不仅仅是在构建时扫描,SBOM 应该被导入到 SBOM 管理平台(如 OWASP Dependency-Track),进行持续的漏洞监控。当新的漏洞发布时,平台应能自动通知受影响的项目。
- 策略即代码 (Policy-as-Code):使用 Open Policy Agent (OPA) 或 Kyverno 等工具定义策略,例如“只允许部署已签署且 SBOM 中没有关键漏洞的工件”。这些策略可以在 CI/CD 管道的末端或部署阶段强制执行。
8.2 密钥管理与 Sigstore 生态系统
- 无密钥签名优先:尽可能使用 Sigstore 的无密钥签名,以避免传统密钥管理带来的复杂性和风险。
- 私有 Rekor 实例:对于有严格合规性要求的企业,可以部署私有 Rekor 实例,以更好地控制日志数据和访问权限。
- Cosign 集成:
cosign不仅可以签署文件,还可以签署容器镜像。将所有软件工件(容器镜像、二进制文件、SBOM、Provenance)都通过cosign签名,建立统一的信任机制。
8.3 供应链安全平台
利用成熟的供应链安全平台,如:
- Sigstore:提供签名、验证和透明日志服务。
- Anchore Enterprise / Syft / Grype:提供全面的 SBOM 生成、漏洞扫描和策略执行功能。
- OWASP Dependency-Track:开源的 SBOM 管理和持续组件分析平台。
- Google Cloud / AWS / Azure 的供应链安全服务:云提供商也开始提供原生服务来帮助用户管理和保护其软件供应链。
这些平台能够集成并自动化上述许多步骤,提供端到端的可见性和控制。
8.4 扩展到其他语言和工件
SLSA 和 SBOM 的概念并非 Go 语言独有,它们适用于任何编程语言和软件工件。syft 和 cosign 等工具通常也支持多种语言和生态系统(如 Java/Maven/Gradle, Node.js/npm, Python/pip, Rust/cargo 等)。推广这些实践到组织内的所有软件项目,以建立统一的安全标准。
8.5 人工审查与安全意识
尽管自动化至关重要,但人工审查和开发者的安全意识仍然不可或缺。定期对关键代码和依赖项进行安全审计,培训开发人员识别和避免供应链风险,是任何安全策略的重要组成部分。
软件供应链安全,刻不容缓
在日益复杂的软件生态系统中,对软件供应链安全的重视已从“锦上添花”转变为“不可或缺”。SLSA 框架与 SBOM 的结合,为我们提供了一套严谨且可操作的方法,以构建可信赖的软件工件。通过在 Go 项目中自动化生成并签署 SBOM,并将其无缝融入 CI/CD 流程,我们不仅提高了软件的透明度和可验证性,更能在源头抵御日益增长的供应链攻击。这是一个持续演进的领域,要求我们不断学习、适应和改进,以确保我们所依赖的软件始终安全可靠。