教程:使用 GitLab CI/CD 构建和签名 Python 包
本教程教你如何为 Python 包实现安全管道。该管道包含使用 GitLab CI/CD 和 Sigstore Cosign 对 Python 包进行加密签名和验证的阶段。
完成本教程后,你将学习如何:
- 使用 GitLab CI/CD 构建和签名 Python 包。
- 使用通用包注册表存储和管理包签名。
- 作为终端用户验证包签名。
包签名有什么好处?
包签名提供几个关键的安全优势:
- 真实性:用户可以验证包来自可信来源。
- 数据完整性:如果在分发过程中包被篡改,将被检测到。
- 不可否认性:可以加密证明包的来源。
- 供应链安全:包签名可以防止供应链攻击和受损的仓库。
开始之前
要完成本教程,你需要:
- 一个 GitLab 账户和测试项目。
- 对 Python 打包、GitLab CI/CD 和包注册表概念的基本熟悉。
步骤
以下是你要做的概述:
设置 Python 项目
首先,创建一个测试项目。在项目根目录中添加一个 pyproject.toml 文件:
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "<my_package>" # 将被 CI/CD 管道动态替换
version = "<1.0.0>" # 将被 CI/CD 管道动态替换
description = "<Your package description>"
readme = "README.md"
requires-python = ">=3.7"
authors = [
{name = "<Your Name>", email = "<your.email@example.com>"},
]
[project.urls]
"Homepage" = "<https://gitlab.com/my_package>" # 将被替换为实际项目 URL确保将 Your Name 和 your.email@example.com 替换为你自己的个人信息。
当你完成以下步骤中的 CI/CD 管道构建时,管道会自动:
- 将
my_package替换为项目名称的规范化版本。 - 将
version更改为与管道版本匹配。 - 将
HomepageURL 更改为你的 GitLab 项目 URL。
添加基础配置
在你的项目根目录中,添加一个 .gitlab-ci.yml 文件。添加以下配置:
variables:
# 所有作业的基础 Python 版本
PYTHON_VERSION: '3.10'
# 包名称和版本
PACKAGE_NAME: ${CI_PROJECT_NAME}
PACKAGE_VERSION: "1.0.0" # 使用语义化版本
# Sigstore 服务 URL
FULCIO_URL: 'https://fulcio.sigstore.dev'
REKOR_URL: 'https://rekor.sigstore.dev'
# Sigstore 验证的标识
CERTIFICATE_IDENTITY: 'https://gitlab.com/${CI_PROJECT_PATH}//.gitlab-ci.yml@refs/heads/${CI_DEFAULT_BRANCH}'
CERTIFICATE_OIDC_ISSUER: 'https://gitlab.com'
# 用于更快构建的 pip 缓存目录
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
# 自动接受 Cosign 的提示
COSIGN_YES: "true"
# 通用包注册表的基础 URL
GENERIC_PACKAGE_BASE_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${PACKAGE_VERSION}"
default:
before_script:
# 在任何作业开始时规范化包名称一次
- export NORMALIZED_NAME=$(echo "${CI_PROJECT_NAME}" | tr '-' '_')
# 基于 Python 的作业模板
.python-job:
image: python:${PYTHON_VERSION}
before_script:
# 首先规范化包名称
- export NORMALIZED_NAME=$(echo "${CI_PROJECT_NAME}" | tr '-' '_')
# 然后安装 Python 依赖
- pip install --upgrade pip
- pip install build twine setuptools wheel
cache:
paths:
- ${PIP_CACHE_DIR}
# Python + Cosign 作业模板
.python+cosign-job:
extends: .python-job
before_script:
# 首先规范化包名称
- export NORMALIZED_NAME=$(echo "${CI_PROJECT_NAME}" | tr '-' '_')
# 然后安装依赖
- apt-get update && apt-get install -y curl wget
- wget -O cosign https://github.com/sigstore/cosign/releases/download/v2.2.3/cosign-linux-amd64
- chmod +x cosign && mv cosign /usr/local/bin/
- export COSIGN_EXPERIMENTAL=1
- pip install --upgrade pip
- pip install build twine setuptools wheel
stages:
- build
- sign
- verify
- publish
- publish_signatures
- consumer_verification这个基础配置:
- 指示管道使用 Python
3.10作为基础镜像以确保一致性 - 设置两个可重用模板:
.python-job用于基本 Python 操作,.python+cosign-job用于签名操作 - 实现 pip 缓存以加速构建
- 通过将连字符转换为下划线来规范化包名称,以实现 Python 兼容性
- 在管道级别定义所有关键变量以便于管理
配置构建阶段
构建阶段构建 Python 分发包。
在你的 .gitlab-ci.yml 文件中,添加以下配置:
build:
extends: .python-job
stage: build
script:
# 使用实际内容初始化 git 仓库
- git init
- git config --global init.defaultBranch main
- git config --global user.email "ci@example.com"
- git config --global user.name "CI"
- git add .
- git commit -m "Initial commit"
# 更新 pyproject.toml 中的包名称、版本和主页 URL
- sed -i "s/name = \".*\"/name = \"${NORMALIZED_NAME}\"/" pyproject.toml
- sed -i "s/version = \".*\"/version = \"${PACKAGE_VERSION}\"/" pyproject.toml
- sed -i "s|\"Homepage\" = \".*\"|\"Homepage\" = \"https://gitlab.com/${CI_PROJECT_PATH}\"|" pyproject.toml
# 调试:显示更新后的文件
- echo "Updated pyproject.toml contents:"
- cat pyproject.toml
# 构建包
- python -m build
artifacts:
paths:
- dist/
- pyproject.toml构建阶段配置:
- 为构建上下文初始化 Git 仓库
- 动态更新
pyproject.toml中的包元数据 - 同时添加 wheel (
.whl) 和源码分发包 (.tar.gz) - 为后续阶段保留构建产物
- 提供故障排除的调试输出
配置签名阶段
签名阶段使用 Sigstore Cosign 对包进行签名。
在你的 .gitlab-ci.yml 文件中,添加以下配置:
sign:
extends: .python+cosign-job
stage: sign
id_tokens:
SIGSTORE_ID_TOKEN:
aud: sigstore
script:
- |
for file in dist/*.whl dist/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
cosign sign-blob --yes \
--fulcio-url=${FULCIO_URL} \
--rekor-url=${REKOR_URL} \
--oidc-issuer $CI_SERVER_URL \
--identity-token $SIGSTORE_ID_TOKEN \
--output-signature "dist/${filename}.sig" \
--output-certificate "dist/${filename}.crt" \
"$file"
# 调试:验证文件是否已创建
echo "Checking generated signature and certificate:"
ls -l "dist/${filename}.sig" "dist/${filename}.crt"
fi
done
artifacts:
paths:
- dist/签名阶段配置:
- 使用 Sigstore 的无密钥签名以增强安全性
- 对 wheel 和源码分发包进行签名
- 创建单独的签名 (
.sig) 和证书 (.crt) 文件 - 使用 OIDC 集成进行身份验证
- 包含签名生成的详细日志记录
配置验证阶段
验证阶段在本地验证签名。
在你的 .gitlab-ci.yml 文件中,添加以下配置:
verify:
extends: .python+cosign-job
stage: verify
script:
- |
failed=0
for file in dist/*.whl dist/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
echo "Verifying file: $file"
echo "Using signature: dist/${filename}.sig"
echo "Using certificate: dist/${filename}.crt"
if ! cosign verify-blob \
--signature "dist/${filename}.sig" \
--certificate "dist/${filename}.crt" \
--certificate-identity "${CERTIFICATE_IDENTITY}" \
--certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \
"$file"; then
echo "Verification failed for $filename"
failed=1
fi
fi
done
if [ $failed -eq 1 ]; then
exit 1
fi验证阶段配置:
- 在签名后立即验证签名
- 检查 wheel 和源码分发包
- 验证证书标识和 OIDC 签发者
- 如果任何验证失败则快速失败
- 提供详细的验证日志
配置发布阶段
发布阶段将包上传到 GitLab PyPI 包注册表。
在你的 .gitlab-ci.yml 文件中,添加以下配置:
publish:
extends: .python-job
stage: publish
script:
- |
# 为 GitLab 包注册表配置 PyPI 设置
cat << EOF > ~/.pypirc
[distutils]
index-servers = gitlab
[gitlab]
repository = ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
username = gitlab-ci-token
password = ${CI_JOB_TOKEN}
EOF
# 使用 twine 上传包
TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token \
twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi \
dist/*.whl dist/*.tar.gz发布阶段配置:
- 配置 PyPI 注册表身份验证
- 使用 GitLab 内置包注册表
- 发布 wheel 和源码分发包
- 使用作业令牌进行安全身份验证
- 创建可重用的
.pypirc配置
配置发布签名阶段
发布签名阶段将签名存储在 GitLab 通用包注册表中。
在你的 .gitlab-ci.yml 文件中,添加以下配置:
publish_signatures:
extends: .python+cosign-job
stage: publish_signatures
script:
- |
for file in dist/*.whl dist/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
ls -l "dist/${filename}.sig" "dist/${filename}.crt"
echo "Publishing signatures for $filename"
echo "Publishing to: ${GENERIC_PACKAGE_BASE_URL}/${filename}.sig"
# 上传签名和证书
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--fail \
--upload-file "dist/${filename}.sig" \
"${GENERIC_PACKAGE_BASE_URL}/${filename}.sig"
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--fail \
--upload-file "dist/${filename}.crt" \
"${GENERIC_PACKAGE_BASE_URL}/${filename}.crt"
fi
done发布签名阶段配置:
- 将签名存储在通用包注册表中
- 保持签名到包的映射
- 使用一致的命名约定作为产物
- 包含签名的大小验证
- 提供详细的上传日志
配置消费者验证阶段
消费者验证阶段模拟终端用户包验证。
在你的 .gitlab-ci.yml 文件中,添加以下配置:
consumer_verification:
extends: .python+cosign-job
stage: consumer_verification
script:
- |
# 为 setuptools_scm 初始化 git 仓库
git init
git config --global init.defaultBranch main
# 创建用于下载包的目录
mkdir -p pkg signatures
# 下载特定的 wheel 版本
pip download --index-url "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple" \
"${NORMALIZED_NAME}==${PACKAGE_VERSION}" --no-deps -d ./pkg --verbose
# 下载特定的源码分发包版本
pip download --no-binary :all: \
--index-url "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple" \
"${NORMALIZED_NAME}==${PACKAGE_VERSION}" --no-deps -d ./pkg --verbose
failed=0
for file in pkg/*.whl pkg/*.tar.gz; do
if [ -f "$file" ]; then
filename=$(basename "$file")
sig_url="${GENERIC_PACKAGE_BASE_URL}/${filename}.sig"
cert_url="${GENERIC_PACKAGE_BASE_URL}/${filename}.crt"
echo "Downloading signatures for $filename"
echo "Signature URL: $sig_url"
echo "Certificate URL: $cert_url"
# 下载签名
curl --fail --silent --show-error \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--output "signatures/${filename}.sig" \
"$sig_url"
curl --fail --silent --show-error \
--header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
--output "signatures/${filename}.crt" \
"$cert_url"
# 验证签名
if ! cosign verify-blob \
--signature "signatures/${filename}.sig" \
--certificate "signatures/${filename}.crt" \
--certificate-identity "${CERTIFICATE_IDENTITY}" \
--certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \
"$file"; then
echo "Signature verification failed"
failed=1
fi
fi
done
if [ $failed -eq 1 ]; then
echo "Verification failed for one or more packages"
exit 1
fi消费者验证阶段配置:
- 模拟真实世界的包安装
- 下载并验证两种包格式
- 使用精确的版本匹配以确保一致性
- 实现全面的错误处理
- 测试完整的验证工作流
作为用户验证包
作为终端用户,你可以通过以下步骤验证包签名:
-
安装 Cosign:
wget -O cosign https://github.com/sigstore/cosign/releases/download/v2.2.3/cosign-linux-amd64 chmod +x cosign && sudo mv cosign /usr/local/bin/Cosign 需要特殊权限进行全局安装。使用
sudo来绕过权限问题。 -
下载包及其签名:
# 你可以在 GitLab 项目主页的项目名称下找到 PROJECT_ID # 下载特定版本的包 pip download your-package-name==1.0.0 --no-deps # FILENAME 将是 pip download 命令的输出 # 例如:your-package-name-1.0.0.tar.gz 或 your-package-name-1.0.0-py3-none-any.whl # 从 GitLab 的通用包注册表下载签名 # 用你的项目详情替换这些值: # GITLAB_URL: 你的 GitLab 实例 URL(例如,https://gitlab.com) # PROJECT_ID: 你的项目 ID 号 # PACKAGE_NAME: 你的包名称 # VERSION: 包版本(例如,1.0.0) # FILENAME: 你下载的包的确切文件名 curl --output "${FILENAME}.sig" \ "${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${VERSION}/${FILENAME}.sig" curl --output "${FILENAME}.crt" \ "${GITLAB_URL}/api/v4/projects/${PROJECT_ID}/packages/generic/${PACKAGE_NAME}/${VERSION}/${FILENAME}.crt" -
验证签名:
# 用项目管道中的值替换 CERTIFICATE_IDENTITY 和 CERTIFICATE_OIDC_ISSUER export CERTIFICATE_IDENTITY="https://gitlab.com/your-group/your-project//.gitlab-ci.yml@refs/heads/main" export CERTIFICATE_OIDC_ISSUER="https://gitlab.com" # 验证 wheel 包 FILENAME="your-package-name-1.0.0-py3-none-any.whl" COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --signature "${FILENAME}.sig" \ --certificate "${FILENAME}.crt" \ --certificate-identity "${CERTIFICATE_IDENTITY}" \ --certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \ "${FILENAME}" # 验证源码分发包 FILENAME="your-package-name-1.0.0.tar.gz" COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --signature "${FILENAME}.sig" \ --certificate "${FILENAME}.crt" \ --certificate-identity "${CERTIFICATE_IDENTITY}" \ --certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}" \ "${FILENAME}"
作为终端用户验证包时:
- 确保包下载与你想要验证的版本完全匹配。
- 分别验证每种包类型(wheel 和源码分发包)。
- 确保证书标识与用于签名的包完全匹配。
- 检查所有 URL 组件是否正确设置。例如,
GITLAB_URL或PROJECT_ID。 - 检查包文件名是否与上传到注册表的完全匹配。
- 使用
COSIGN_EXPERIMENTAL=1功能标志进行无密钥验证。此标志是必需的。 - 了解验证失败可能表示篡改或不正确的证书和签名对。
- 记住项目管道中的证书标识和签发者值。
故障排除
完成本教程时,你可能会遇到以下错误:
错误:404 Not Found
如果你遇到 404 Not Found 错误页面:
- 仔细检查所有 URL 组件。
- 验证注册表中是否存在该包版本。
- 确保文件名完全匹配,包括版本和平台标签。
验证失败
如果签名验证失败,请确保:
CERTIFICATE_IDENTITY与签名管道匹配。CERTIFICATE_OIDC_ISSUER正确。- 签名和证书对与包正确对应。
权限被拒绝
如果你遇到权限问题:
- 检查你是否有权访问包注册表。
- 如果注册表是私有的,验证身份验证。
- 安装 Cosign 时使用正确的文件权限。
身份验证问题
如果你遇到身份验证问题:
- 检查
CI_JOB_TOKEN权限。 - 验证注册表身份验证配置。
- 验证项目的访问设置。
验证包配置和管道设置
检查包配置。确保:
- 包名称使用下划线 (
_),而不是连字符 (-)。 - 版本字符串使用有效的 PEP 440。
pyproject.toml文件格式正确。
检查管道设置。确保:
- OIDC 配置正确。
- 作业依赖关系正确设置。
- 所需权限已就位。