Skip to content

Commit e91fbc7

Browse files
committed
Add code signing of release binaries via cargo-code-sign
1 parent 355fe05 commit e91fbc7

File tree

9 files changed

+286
-20
lines changed

9 files changed

+286
-20
lines changed

.github/workflows/build-release-binaries.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,45 @@ jobs:
100100
- name: "Install cargo extensions"
101101
shell: bash
102102
run: scripts/install-cargo-extensions.sh
103+
- name: "Prepare macOS signing certificate"
104+
run: |
105+
set -euo pipefail
106+
107+
CERT_NAME="uv-codesign-ci"
108+
CERT_DIR="$RUNNER_TEMP/codesign-cert"
109+
mkdir -p "$CERT_DIR"
110+
111+
openssl req -x509 -newkey rsa:2048 -sha256 -days 7 -nodes \
112+
-keyout "$CERT_DIR/key.pem" \
113+
-out "$CERT_DIR/cert.pem" \
114+
-subj "/CN=$CERT_NAME" \
115+
-addext "extendedKeyUsage=codeSigning" \
116+
-addext "keyUsage=digitalSignature"
117+
118+
P12_PASSWORD="$(uuidgen | tr -d '-')"
119+
120+
# LibreSSL (shipped with macOS) doesn't support -legacy; OpenSSL 3.x
121+
# requires it for macOS keychain compatibility.
122+
LEGACY_FLAG=""
123+
if openssl version 2>&1 | grep -q "^OpenSSL 3"; then
124+
LEGACY_FLAG="-legacy"
125+
fi
126+
127+
openssl pkcs12 -export $LEGACY_FLAG \
128+
-inkey "$CERT_DIR/key.pem" \
129+
-in "$CERT_DIR/cert.pem" \
130+
-name "$CERT_NAME" \
131+
-out "$CERT_DIR/cert.p12" \
132+
-passout pass:"$P12_PASSWORD"
133+
134+
CERT_B64="$(base64 < "$CERT_DIR/cert.p12" | tr -d '\n')"
135+
CERT_SHA1="$(openssl x509 -in "$CERT_DIR/cert.pem" -noout -fingerprint -sha1 | cut -d= -f2 | tr -d ':')"
136+
137+
{
138+
echo "CODESIGN_IDENTITY=$CERT_SHA1"
139+
echo "CODESIGN_CERTIFICATE=$CERT_B64"
140+
echo "CODESIGN_CERTIFICATE_PASSWORD=$P12_PASSWORD"
141+
} >> "$GITHUB_ENV"
103142
104143
# uv
105144
- name: "Build wheels - x86_64"
@@ -166,6 +205,45 @@ jobs:
166205
- name: "Install cargo extensions"
167206
shell: bash
168207
run: scripts/install-cargo-extensions.sh
208+
- name: "Prepare macOS signing certificate"
209+
run: |
210+
set -euo pipefail
211+
212+
CERT_NAME="uv-codesign-ci"
213+
CERT_DIR="$RUNNER_TEMP/codesign-cert"
214+
mkdir -p "$CERT_DIR"
215+
216+
openssl req -x509 -newkey rsa:2048 -sha256 -days 7 -nodes \
217+
-keyout "$CERT_DIR/key.pem" \
218+
-out "$CERT_DIR/cert.pem" \
219+
-subj "/CN=$CERT_NAME" \
220+
-addext "extendedKeyUsage=codeSigning" \
221+
-addext "keyUsage=digitalSignature"
222+
223+
P12_PASSWORD="$(uuidgen | tr -d '-')"
224+
225+
# LibreSSL (shipped with macOS) doesn't support -legacy; OpenSSL 3.x
226+
# requires it for macOS keychain compatibility.
227+
LEGACY_FLAG=""
228+
if openssl version 2>&1 | grep -q "^OpenSSL 3"; then
229+
LEGACY_FLAG="-legacy"
230+
fi
231+
232+
openssl pkcs12 -export $LEGACY_FLAG \
233+
-inkey "$CERT_DIR/key.pem" \
234+
-in "$CERT_DIR/cert.pem" \
235+
-name "$CERT_NAME" \
236+
-out "$CERT_DIR/cert.p12" \
237+
-passout pass:"$P12_PASSWORD"
238+
239+
CERT_B64="$(base64 < "$CERT_DIR/cert.p12" | tr -d '\n')"
240+
CERT_SHA1="$(openssl x509 -in "$CERT_DIR/cert.pem" -noout -fingerprint -sha1 | cut -d= -f2 | tr -d ':')"
241+
242+
{
243+
echo "CODESIGN_IDENTITY=$CERT_SHA1"
244+
echo "CODESIGN_CERTIFICATE=$CERT_B64"
245+
echo "CODESIGN_CERTIFICATE_PASSWORD=$P12_PASSWORD"
246+
} >> "$GITHUB_ENV"
169247
170248
# uv
171249
- name: "Build wheels - aarch64"
@@ -256,6 +334,25 @@ jobs:
256334
- name: "Install cargo extensions"
257335
shell: bash
258336
run: scripts/install-cargo-extensions.sh
337+
- name: "Prepare Windows signing certificate"
338+
shell: pwsh
339+
run: |
340+
$cert = New-SelfSignedCertificate `
341+
-Type CodeSigningCert `
342+
-Subject "CN=uv-codesign-ci" `
343+
-CertStoreLocation "Cert:\CurrentUser\My" `
344+
-NotAfter (Get-Date).AddDays(7)
345+
$passwordPlain = [Guid]::NewGuid().ToString("N")
346+
$password = ConvertTo-SecureString -String $passwordPlain -Force -AsPlainText
347+
$pfxPath = Join-Path $env:RUNNER_TEMP "uv-codesign-ci.pfx"
348+
349+
Export-PfxCertificate `
350+
-Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" `
351+
-FilePath $pfxPath `
352+
-Password $password | Out-Null
353+
354+
"SIGNTOOL_CERTIFICATE_PATH=$pfxPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
355+
"SIGNTOOL_CERTIFICATE_PASSWORD=$passwordPlain" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
259356
260357
# uv
261358
- name: "Build wheels"

scripts/cargo-auditable.cmd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@echo off
2+
REM Cargo wrapper that runs `cargo auditable` to embed SBOM metadata.
3+
REM See cargo-auditable.sh for the full explanation.
4+
5+
cargo.exe auditable %*

scripts/cargo-auditable.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env sh
2+
## Cargo wrapper that runs `cargo auditable` to embed SBOM metadata.
3+
##
4+
## Used as the inner build command for `cargo-code-sign`.
5+
##
6+
## Usage:
7+
##
8+
## CARGO_CODE_SIGN_CARGO="$PWD/scripts/cargo-auditable.sh" cargo-code-sign code-sign ...
9+
10+
set -eu
11+
12+
exec cargo auditable "$@"

scripts/cargo-code-sign.cmd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@echo off
2+
REM Cargo wrapper that signs binaries after building via `cargo-code-sign`.
3+
REM See cargo-code-sign.sh for the full explanation.
4+
5+
cargo-code-sign.exe code-sign %*

scripts/cargo-code-sign.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env sh
2+
## Cargo wrapper that signs binaries after building via `cargo-code-sign`.
3+
##
4+
## Uses `CARGO_CODE_SIGN_CARGO` to determine the inner cargo command.
5+
## If unset, falls back to plain `cargo`.
6+
##
7+
## Usage:
8+
##
9+
## CARGO_CODE_SIGN_CARGO="$PWD/scripts/cargo-auditable.sh" \
10+
## scripts/cargo-code-sign.sh build --release
11+
12+
set -eu
13+
14+
exec cargo-code-sign code-sign "$@"

scripts/cargo.cmd

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
@echo off
2-
REM Wrapper script that invokes `cargo auditable` instead of plain `cargo`.
2+
REM Top-level cargo wrapper for release builds.
33
REM
4-
REM Use `scripts/install-cargo-extensions.sh` to install the dependencies.
5-
REM
6-
REM Usage:
7-
REM
8-
REM set CARGO=%CD%\scripts\cargo.cmd
9-
REM cargo build --release
4+
REM Chains `cargo-code-sign` (post-build binary signing) with `cargo-auditable`
5+
REM (SBOM embedding). See cargo.sh for the full explanation.
106

11-
if defined REAL_CARGO (
12-
"%REAL_CARGO%" auditable %*
13-
) else (
14-
cargo.exe auditable %*
15-
)
7+
set CARGO_CODE_SIGN_CARGO=%~dp0cargo-auditable.cmd
8+
%~dp0cargo-code-sign.cmd %*

scripts/cargo.sh

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
#!/usr/bin/env sh
2-
## Wrapper script that invokes `cargo auditable` instead of plain `cargo`.
2+
## Top-level cargo wrapper for release builds.
3+
##
4+
## Chains `cargo-code-sign` (post-build binary signing) with `cargo-auditable`
5+
## (SBOM embedding):
6+
##
7+
## maturin -> cargo.sh -> cargo-code-sign -> cargo-auditable -> cargo
38
##
49
## Use `scripts/install-cargo-extensions.sh` to install the dependencies.
510
##
611
## Usage:
712
##
8-
## CARGO="$PWD/scripts/cargo.sh" cargo build --release
13+
## CARGO="$PWD/scripts/cargo.sh" maturin build --release
914

1015
set -eu
1116

12-
if [ -n "${REAL_CARGO:-}" ]; then
13-
exec "$REAL_CARGO" auditable "$@"
14-
else
15-
exec cargo auditable "$@"
16-
fi
17+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18+
19+
# Tell cargo-code-sign to use cargo-auditable as the inner build command.
20+
export CARGO_CODE_SIGN_CARGO="${SCRIPT_DIR}/cargo-auditable.sh"
21+
22+
exec "${SCRIPT_DIR}/cargo-code-sign.sh" "$@"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env bash
2+
## Check that release artifacts from a CI run are code-signed.
3+
##
4+
## Downloads macOS and Windows artifacts and wheels from the given GitHub
5+
## Actions run, extracts binaries, and verifies:
6+
## - macOS: codesign identity signature (not ad-hoc)
7+
## - Windows: Authenticode signature present
8+
##
9+
## Usage:
10+
## scripts/check-release-artifacts-signed.sh <run-id>
11+
12+
set -euo pipefail
13+
14+
if [ $# -lt 1 ]; then
15+
echo "Usage: $0 <run-id>" >&2
16+
exit 1
17+
fi
18+
19+
missing=()
20+
command -v gh >/dev/null 2>&1 || missing+=(gh)
21+
command -v codesign >/dev/null 2>&1 || missing+=(codesign)
22+
command -v osslsigncode >/dev/null 2>&1 || missing+=(osslsigncode)
23+
24+
if [ ${#missing[@]} -gt 0 ]; then
25+
echo "error: missing required tools: ${missing[*]}" >&2
26+
echo "" >&2
27+
echo "Install with:" >&2
28+
for tool in "${missing[@]}"; do
29+
case "$tool" in
30+
gh) echo " brew install gh" >&2 ;;
31+
codesign) echo " (requires macOS)" >&2 ;;
32+
osslsigncode) echo " brew install osslsigncode" >&2 ;;
33+
esac
34+
done
35+
exit 1
36+
fi
37+
38+
RUN_ID="$1"
39+
WORK_DIR="$(mktemp -d)"
40+
trap 'rm -rf "$WORK_DIR"' EXIT
41+
42+
PASS=0
43+
FAIL=0
44+
45+
pass() { echo "PASS $1"; PASS=$((PASS + 1)); }
46+
fail() { echo "FAIL $1"; FAIL=$((FAIL + 1)); }
47+
48+
check_macos() {
49+
local binary="$1"
50+
local label="$2"
51+
local info
52+
info=$(codesign -dv "$binary" 2>&1) || true
53+
if echo "$info" | grep -q "Signature=adhoc"; then
54+
fail "$label (ad-hoc, not identity-signed)"
55+
elif sig_size=$(echo "$info" | grep "Signature size=" | sed 's/.*Signature size=//'); then
56+
pass "$label (identity-signed, size=$sig_size)"
57+
else
58+
fail "$label (not signed)"
59+
fi
60+
}
61+
62+
check_windows() {
63+
local binary="$1"
64+
local label="$2"
65+
local output
66+
output=$(osslsigncode verify -in "$binary" 2>&1) || true
67+
if echo "$output" | grep -q "Signer's certificate:"; then
68+
local subject
69+
subject=$(echo "$output" | grep "Subject:" | head -1 | sed 's/.*Subject: //')
70+
pass "$label (Authenticode, $subject)"
71+
else
72+
fail "$label (not Authenticode signed)"
73+
fi
74+
}
75+
76+
echo "Fetching artifacts for run $RUN_ID..."
77+
ALL_ARTIFACTS=$(gh api "repos/{owner}/{repo}/actions/runs/$RUN_ID/artifacts" \
78+
--paginate --jq '.artifacts[].name')
79+
80+
echo ""
81+
82+
for artifact in $ALL_ARTIFACTS; do
83+
# Only check macOS and Windows archives and wheels.
84+
case "$artifact" in
85+
artifacts-*apple-darwin*|artifacts-macos-*) check=check_macos ;;
86+
artifacts-*windows*|artifacts-*win*) check=check_windows ;;
87+
wheels_uv-*apple-darwin*|wheels_uv-macos-*) check=check_macos ;;
88+
wheels_uv-*windows*|wheels_uv-*win*) check=check_windows ;;
89+
*) continue ;;
90+
esac
91+
92+
dest="$WORK_DIR/$artifact"
93+
mkdir -p "$dest"
94+
if ! gh run download "$RUN_ID" -n "$artifact" -D "$dest"; then
95+
fail "$artifact (download failed)"
96+
continue
97+
fi
98+
99+
# Extract everything: tar.gz archives, zip archives, and wheels.
100+
for tarball in "$dest"/*.tar.gz; do
101+
[ -f "$tarball" ] || continue
102+
tar xzf "$tarball" -C "$dest"
103+
done
104+
for zip in "$dest"/*.zip "$dest"/*.whl; do
105+
[ -f "$zip" ] || continue
106+
unzip -qo "$zip" -d "$dest"
107+
done
108+
109+
# Check each binary. The label shows the archive/wheel filename and binary name,
110+
# e.g. "uv-x86_64-apple-darwin.tar.gz uv" or "uv-0.10.8-py3-none-win_amd64.whl uv.exe".
111+
while IFS= read -r binary; do
112+
bin_name=$(basename "$binary")
113+
# Walk up to find the archive or wheel this binary came from.
114+
archive=""
115+
for f in "$dest"/*.tar.gz "$dest"/*.zip "$dest"/*.whl; do
116+
[ -f "$f" ] && archive=$(basename "$f") && break
117+
done
118+
$check "$binary" "${archive:-$artifact} / $bin_name"
119+
done < <(find "$dest" \( -name "uv" -o -name "uvx" -o -name "uv.exe" -o -name "uvx.exe" -o -name "uvw.exe" \) -type f ! -name "*.sha256")
120+
done
121+
122+
echo ""
123+
echo "PASS $PASS / FAIL $FAIL"
124+
125+
if [ "$FAIL" -gt 0 ]; then
126+
exit 1
127+
fi

scripts/install-cargo-extensions.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env sh
22
## Install cargo extensions for release builds.
33
##
4-
## Installs cargo-auditable for SBOM embedding.
4+
## Installs cargo-auditable for SBOM embedding and cargo-code-sign for binary signing.
55
##
66
## Includes handling for cross-build containers in our release workflow.
77
##
@@ -20,6 +20,11 @@ CARGO_AUDITABLE_INSTALL="cargo install cargo-auditable \
2020
--git https://github.com/rust-secure-code/cargo-auditable.git \
2121
--rev 7df767ff9e844d742d7223c62b80353da0f18433"
2222

23+
CARGO_CODE_SIGN_INSTALL="cargo install cargo-code-sign \
24+
--locked \
25+
--git https://github.com/zanieb/cargo-code-sign \
26+
--rev 1a305cd50144d46fa2dbc6614c8692ac36daacde"
27+
2328
# In Linux containers running on x86_64, build a static musl binary so the installed tool works in
2429
# musl-based environments (Alpine, etc.).
2530
#
@@ -29,6 +34,8 @@ if [ "$(uname -m 2>/dev/null)" = "x86_64" ] && [ "$(uname -s 2>/dev/null)" = "Li
2934
MUSL_TARGET="x86_64-unknown-linux-musl"
3035
rustup target add "$MUSL_TARGET"
3136
CC=gcc $CARGO_AUDITABLE_INSTALL --target "$MUSL_TARGET"
37+
CC=gcc $CARGO_CODE_SIGN_INSTALL --target "$MUSL_TARGET"
3238
else
3339
$CARGO_AUDITABLE_INSTALL
40+
$CARGO_CODE_SIGN_INSTALL
3441
fi

0 commit comments

Comments
 (0)