什么是 ‘Supply Chain Security (SLSA)’:在 Go 项目中自动化生成并签署 SBOM 的标准流程

软件供应链安全 (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.modgo.sum 文件。

syft packages dir:. --scope all-dependencies --output cyclonedx-json > sbom.json

参数解释:

  • packages dir:.:指示 syft 分析当前目录。
  • --scope all-dependencies:确保包含所有直接和传递依赖。
  • --output cyclonedx-json:指定输出格式为 CycloneDX JSON。你也可以选择 spdx-jsoncyclonedx-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.json

    grype 会根据 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 中。这提供了不可否认的证据,并允许任何人审计签名历史。

无密钥签名的流程:

  1. 身份验证cosign 通过 OIDC 与身份提供者(例如 GitHub Actions)进行交互,获取一个身份令牌。
  2. 证书请求cosign 使用该身份令牌向 Fulcio 请求一个短期签名证书。Fulcio 验证令牌并颁发证书。
  3. 签名cosign 使用私有密钥(在本地临时生成并立即丢弃)对 SBOM 进行签名,并使用 Fulcio 颁发的证书来证明签名者的身份。
  4. 日志记录:签名及其相关的证书和 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."

工作流解释:

  1. permissions
    • contents: write:允许工作流将文件推送到仓库,虽然我们主要通过 GitHub Artifacts 和 GHCR 进行存储,但某些情况下可能需要此权限。
    • id-token: write:这是进行 Sigstore 无密钥签名的关键。它允许工作流请求一个 OIDC 令牌,用于向 Fulcio 证明其身份。
    • packages: write:允许工作流将工件推送到 GitHub Packages (GHCR)。
  2. Checkout code:拉取 Go 项目的源代码。
  3. 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 的名称。
    • 此步骤的输出会包含生成的工件路径。
  4. Download Generated Artifacts:此步骤是可选的,仅用于在 CI 内部验证。在实际消费者场景中,用户会直接从 GitHub Actions Artifacts 或 GHCR 下载。
  5. 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 验证流程概述

消费者下载软件工件后,将执行以下验证步骤:

  1. 验证应用程序二进制文件的 SLSA Provenance:确保应用程序的构建过程符合 SLSA 规范,并且未被篡改。
  2. 验证 SBOM 的签名:确保 SBOM 文件本身未被篡改,并且是由可信的构建系统或发行方生成的。
  3. 分析 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。否则,可能需要指定 branchcommit
  • --provenance-path:SLSA Provenance 文件的本地路径。
  • --artifact-path:要验证的应用程序二进制文件的本地路径。

slsa-verifier 会执行一系列检查:

  1. 验证 Provenance 文件本身的签名,确保其来自可信的 GitHub Actions 运行。
  2. 检查 Provenance 中记录的构建步骤、输入和输出是否与期望的一致。
  3. 验证 Provenance 中记录的应用程序哈希值是否与本地下载的应用程序哈希值匹配。
  4. 确保 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 语言独有,它们适用于任何编程语言和软件工件。syftcosign 等工具通常也支持多种语言和生态系统(如 Java/Maven/Gradle, Node.js/npm, Python/pip, Rust/cargo 等)。推广这些实践到组织内的所有软件项目,以建立统一的安全标准。

8.5 人工审查与安全意识

尽管自动化至关重要,但人工审查和开发者的安全意识仍然不可或缺。定期对关键代码和依赖项进行安全审计,培训开发人员识别和避免供应链风险,是任何安全策略的重要组成部分。

软件供应链安全,刻不容缓

在日益复杂的软件生态系统中,对软件供应链安全的重视已从“锦上添花”转变为“不可或缺”。SLSA 框架与 SBOM 的结合,为我们提供了一套严谨且可操作的方法,以构建可信赖的软件工件。通过在 Go 项目中自动化生成并签署 SBOM,并将其无缝融入 CI/CD 流程,我们不仅提高了软件的透明度和可验证性,更能在源头抵御日益增长的供应链攻击。这是一个持续演进的领域,要求我们不断学习、适应和改进,以确保我们所依赖的软件始终安全可靠。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注