Files
voculinux/packaging/void/container-repo.sh
T
soubarnak 9bc04936b6 feat(void): add signed xbps repository pipeline
Build every template in smoke-gate order, index the full repository and
sign packages plus metadata with the RSA4096 key, in the pinned Void
container via podman. Fail-closed without a key: no key file, no repo.

Ticket #29 (needs #27, #28).
2026-11-06 06:29:22 +05:30

130 lines
4.7 KiB
Bash
Executable File

#!/bin/sh
# Repository half of packaging/void/repo.sh (ticket #29). Runs inside the
# pinned Void container as root: system setup, the void-packages checkout
# at the pinned commit, then every template built in smoke-gate order as
# the unprivileged builder user (xbps-src refuses root), then index, sign,
# verify and stage as root again.
#
# Not executed directly: repo.sh mounts this file and runs it. VOID_COMMIT,
# PKGS and SIGNEDBY arrive via the environment; the RSA4096 private key is
# mounted read-only at /run/xbps-sign-key.pem (repo.sh validated it before
# any container ran).
#
# Keeping this a real file (instead of an inline `sh -c` string) means
# normal quoting, heredocs, and shellcheck apply.
set -eu
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
KEY=/run/xbps-sign-key.pem
OUT=/mnt/out
REPO="$OUT/repo"
KEYS="$OUT/keys"
[ -r "$KEY" ] || {
echo "container-repo.sh: signing key not mounted at $KEY" >&2
exit 1
}
xbps-install -Suy xbps >/dev/null
xbps-install -y git base-devel xtools binutils zstd openssl >/dev/null
git clone --quiet --depth 1 https://github.com/void-linux/void-packages /void-packages
git -C /void-packages fetch --quiet --depth 1 origin "$VOID_COMMIT"
git -C /void-packages checkout --quiet "$VOID_COMMIT"
cp -r /overlay/. /void-packages/srcpkgs/
id builder >/dev/null 2>&1 || useradd -m builder
chown builder:builder /void-packages
echo "== build every template in order: $PKGS"
cd /void-packages
# shellcheck disable=SC2086
for PKG in $PKGS; do
echo "== xlint $PKG"
su -s /bin/sh builder -c "cd /void-packages && xlint 'srcpkgs/$PKG/template'"
echo "== xbps-src pkg $PKG"
su -s /bin/sh builder -c "cd /void-packages && ./xbps-src pkg '$PKG'"
done
echo "== collect built packages"
mkdir -p "$REPO" "$KEYS"
# shellcheck disable=SC2086
for PKG in $PKGS; do
PKGFILE=$(ls /void-packages/hostdir/binpkgs/"$PKG"-*.x86_64.xbps 2>/dev/null | head -n1)
[ -n "$PKGFILE" ] || {
echo "FAIL: $PKG (built package missing from hostdir/binpkgs)" >&2
exit 1
}
echo "collected: $(basename "$PKGFILE")"
cp "$PKGFILE" "$REPO/"
done
echo "== index the full repository"
# shellcheck disable=SC2086
xbps-rindex -a "$REPO"/*.x86_64.xbps
echo "== sign every binary package"
xbps-rindex -S --privkey "$KEY" --signedby "$SIGNEDBY" "$REPO"/*.x86_64.xbps
echo "== sign the repository metadata"
xbps-rindex -s --privkey "$KEY" --signedby "$SIGNEDBY" "$REPO"
echo "== prove an unsigned install is refused"
set +e
PROBE=$(xbps-install -r /tmp/keyprobe --repository="$REPO" -Sy voculinux </dev/null 2>&1)
PROBE_RC=$?
set -e
if [ "$PROBE_RC" -eq 0 ] || ! printf '%s' "$PROBE" | grep -q "RSA signed"; then
echo "FAIL: unsigned install was not refused — signatures are not enforced" >&2
printf '%s\n' "$PROBE" >&2
exit 1
fi
echo "unsigned install refused as required"
FP=$(printf '%s' "$PROBE" | grep -i Fingerprint | awk '{print $2}' | head -n1)
[ -n "$FP" ] || {
echo "FAIL: could not read the key fingerprint from the refusal" >&2
exit 1
}
echo "key fingerprint: $FP"
echo "== stage the client key"
openssl rsa -in "$KEY" -pubout -out /tmp/repo-pub.pem 2>/dev/null
PUB64=$(base64 -w0 /tmp/repo-pub.pem)
# XML-escape the signer identity: xbps writes &lt;/&gt; itself on import
# and rejects a plist carrying raw angle brackets.
ESCAPED_SIGNEDBY=$(printf '%s' "$SIGNEDBY" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')
{
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
printf '%s\n' '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
printf '%s\n' '<plist version="1.0">' '<dict>' ' <key>public-key</key>'
printf ' <data>%s</data>\n' "$PUB64"
printf '%s\n' ' <key>public-key-size</key>' ' <integer>4096</integer>' ' <key>signature-by</key>'
printf ' <string>%s</string>\n' "$ESCAPED_SIGNEDBY"
printf '%s\n' '</dict>' '</plist>'
} > "$KEYS/$FP.plist"
{
printf 'signed-by: %s\n' "$SIGNEDBY"
printf 'fingerprint: %s\n' "$FP"
} > "$KEYS/FINGERPRINT"
echo "staged: keys/$FP.plist"
echo "== verify: install the app from the signed repo in a clean rootdir"
VERIFY=/tmp/verify-install
mkdir -p "$VERIFY/var/db/xbps/keys"
# The app's full dependency tree resolves from the official remote, so the
# verify rootdir trusts the official keys too; our repo stays enforced
# because its packages verify only against the staged key below.
cp /var/db/xbps/keys/*.plist "$VERIFY/var/db/xbps/keys/"
cp "$KEYS/$FP.plist" "$VERIFY/var/db/xbps/keys/"
xbps-install -y -r "$VERIFY" \
--repository="$REPO" \
--repository=https://repo-default.voidlinux.org/current \
-Sy voculinux
xbps-query -r "$VERIFY" -l | grep -q "^ii voculinux-" || {
echo "FAIL: voculinux not installed in the verify rootdir" >&2
exit 1
}
echo "verified: voculinux installs from the signed repository"
ls "$REPO"
echo "SIGNED REPO OK: fingerprint $FP"