Fix audit findings: bakery index signing, artifact checksums, stale theme docs
- Add minisign-based signing/verification for the bakery index: scripts/gen-index.sh signs index.json (MINISIGN_SEC_KEY env var, dormant no-op with a loud warning until a key is provisioned); bakery/src/manifest.rs fetches index.json.minisig and verifies it with minisign-verify against a hardcoded PUBKEY before parsing/caching, and re-verifies the cached copy on every load (falls back to one re-fetch if the cache predates signing or fails verification; a fresh fetch that fails verification is a hard error). - Close the previously-unchecksummed config-example and systemd-unit downloads in bakery/src/install.rs (scaffold_config, install_service): index.json now carries `sha256`/`example_sha256` for these artifacts (computed in gen-index.sh), verified via the same download::verify_sha256 used for binaries. Downloads without a matching sha256 in the index are refused rather than installed unverified. - scripts/get.sh now verifies the bakery release binary itself against a pinned minisign public key before installing it (falls back to the existing sha256-only check with a loud warning if no .minisig is published yet or minisign isn't installed; a present-but-invalid signature is a hard failure). - Add dormant "sign release binary" steps to the bakery and bread-theme release workflows (.github/workflows/release.yml, .forgejo/workflows/release-bread-theme.yml), gated on secrets that are not yet configured — binaries ship unsigned exactly as before until the owner wires up the secret. - .gitignore: add *.minisign-sec / minisign.key so the signing key can never be committed by accident. - bread-theme: fix stale docs describing a "Catppuccin Mocha fallback" (BREAD_DESIGN_SYSTEM.md, README.md, Cargo.toml/bakery.toml/registry descriptions) — the actual implementation (palette.rs) uses a fixed BOS dark base with only accent colors from pywal. - bread-theme: fix the legacy css_vars() path, which had its own hand-written @define-color block that predated the `accent` and computed `on-*` ink colors used by the rest of the stylesheet — any caller whose CSS referenced those names against css_vars()'s output would hit undefined colors (the illegible-text bug). css_vars() now delegates to the same define_colors() the full stylesheet uses, so the two can't drift apart again.
This commit is contained in:
parent
fa0597f482
commit
394a252f9e
18 changed files with 472 additions and 84 deletions
|
|
@ -119,8 +119,12 @@ with open('${bakery_toml}', 'rb') as f:
|
|||
print(json.dumps(d.get('bread_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
# [[service]] entries → [{unit, enable}]
|
||||
services="$(python3 -c "
|
||||
# [[service]] entries → [{unit, enable, sha256}]. sha256 comes from the
|
||||
# actual unit file shipped in this version dir — the same
|
||||
# artifact-integrity guarantee binaries already get. A missing unit file
|
||||
# gets an empty sha256; install.rs refuses to install an unverified
|
||||
# download rather than silently skipping the check.
|
||||
service_units="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
|
|
@ -128,7 +132,24 @@ svcs = d.get('service', [])
|
|||
print(json.dumps([{'unit': s['unit'], 'enable': s.get('enable', False)} for s in svcs]))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
# [config] → {dir, example?} or null
|
||||
services="[]"
|
||||
while IFS= read -r svc_entry; do
|
||||
[[ -z "${svc_entry}" ]] && continue
|
||||
unit_name="$(echo "${svc_entry}" | jq -r '.unit')"
|
||||
enable="$(echo "${svc_entry}" | jq -r '.enable')"
|
||||
unit_path="${version_dir}/${unit_name}"
|
||||
unit_sha256=""
|
||||
if [[ -f "${unit_path}" ]]; then
|
||||
unit_sha256="$(sha256sum "${unit_path}" | awk '{print $1}')"
|
||||
else
|
||||
echo " warning: service unit '${unit_name}' not found at ${unit_path}" >&2
|
||||
fi
|
||||
svc_json="$(jq -n --arg unit "${unit_name}" --argjson enable "${enable}" --arg sha256 "${unit_sha256}" \
|
||||
'{unit: $unit, enable: $enable, sha256: $sha256}')"
|
||||
services="$(jq -n --argjson arr "${services}" --argjson e "${svc_json}" '$arr + [$e]')"
|
||||
done < <(echo "${service_units}" | jq -c '.[]')
|
||||
|
||||
# [config] → {dir, example?, example_sha256?} or null
|
||||
config="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
|
|
@ -142,6 +163,19 @@ if cfg:
|
|||
else:
|
||||
print('null')
|
||||
" 2>/dev/null || echo "null")"
|
||||
if [[ "${config}" != "null" ]]; then
|
||||
example_name="$(echo "${config}" | jq -r '.example // empty')"
|
||||
if [[ -n "${example_name}" ]]; then
|
||||
example_path="${version_dir}/${example_name}"
|
||||
example_sha256=""
|
||||
if [[ -f "${example_path}" ]]; then
|
||||
example_sha256="$(sha256sum "${example_path}" | awk '{print $1}')"
|
||||
else
|
||||
echo " warning: config.example '${example_name}' not found at ${example_path}" >&2
|
||||
fi
|
||||
config="$(echo "${config}" | jq -c --arg sha "${example_sha256}" '. + {example_sha256: $sha}')"
|
||||
fi
|
||||
fi
|
||||
|
||||
post_install="$(python3 -c "
|
||||
import tomllib, json
|
||||
|
|
@ -194,3 +228,42 @@ jq -n \
|
|||
> "${OUT}"
|
||||
|
||||
echo "wrote ${OUT}"
|
||||
|
||||
# Sign the index so `bakery` can verify it before trusting a single byte.
|
||||
# Every artifact sha256 and post_install hook string lives inside index.json,
|
||||
# so a valid signature over these raw bytes transitively covers all of it —
|
||||
# no separate per-artifact signing is needed.
|
||||
#
|
||||
# MINISIGN_SEC_KEY must point at the *secret* key file generated with
|
||||
# `minisign -G`. It is intentionally never read from inside either git repo;
|
||||
# point it at wherever the key actually lives on the machine that runs this
|
||||
# script (e.g. a root-only path on hestia), and set MINISIGN_SEC_KEY_PASSWORD
|
||||
# too if the key was generated with a password.
|
||||
#
|
||||
# This step is a no-op (with a loud warning) if the key isn't configured, so
|
||||
# existing unsigned publishing flows don't break until the key is actually
|
||||
# wired up — see the handoff note in the fix commit for this repo.
|
||||
if [[ -n "${MINISIGN_SEC_KEY:-}" ]]; then
|
||||
if [[ ! -f "${MINISIGN_SEC_KEY}" ]]; then
|
||||
echo "ERROR: MINISIGN_SEC_KEY=${MINISIGN_SEC_KEY} does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v minisign >/dev/null 2>&1; then
|
||||
echo "ERROR: MINISIGN_SEC_KEY is set but the 'minisign' binary is not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
sign_args=(-S -s "${MINISIGN_SEC_KEY}" -m "${OUT}" -x "${OUT}.minisig")
|
||||
if [[ -n "${MINISIGN_SEC_KEY_PASSWORD:-}" ]]; then
|
||||
MINISIGN_PASSWORD="${MINISIGN_SEC_KEY_PASSWORD}" minisign "${sign_args[@]}" </dev/null
|
||||
else
|
||||
# -W: the key has no password (matches how CI-facing signing keys are
|
||||
# normally generated, since there's no human to type a passphrase).
|
||||
minisign -W "${sign_args[@]}" </dev/null
|
||||
fi
|
||||
echo "signed ${OUT} -> ${OUT}.minisig"
|
||||
else
|
||||
echo "WARNING: MINISIGN_SEC_KEY not set — index.json was NOT signed." >&2
|
||||
echo " bakery clients built with signature verification will reject" >&2
|
||||
echo " this index. Set MINISIGN_SEC_KEY before running this in" >&2
|
||||
echo " production once the signing key has been provisioned." >&2
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@
|
|||
# Or: curl -sSfL https://breadway.dev/get | sh
|
||||
set -eu
|
||||
|
||||
# Pinned minisign public key for the bakery release binary. Matches the
|
||||
# PUBKEY constant in bakery/src/manifest.rs (same keypair signs both
|
||||
# index.json and the bakery binary itself). Do not source this from the
|
||||
# network — it must be baked into this script so a compromised dl server
|
||||
# can't swap it out along with a malicious binary.
|
||||
BAKERY_MINISIGN_PUBKEY="RWRh2Zr5SUinvVFCtD7S7HwGjfrye6j31Xq2mYXRdkGFDWe3yHF7W11K"
|
||||
|
||||
BAKERY_VERSION="${BAKERY_VERSION:-latest}"
|
||||
BIN_DIR="${BAKERY_BIN_DIR:-$HOME/.local/bin}"
|
||||
|
||||
|
|
@ -19,12 +26,16 @@ if [ "${BAKERY_VERSION}" = "latest" ]; then
|
|||
DL_PRIMARY="https://dl.breadway.dev/bakery/latest/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/latest/download/bakery-x86_64"
|
||||
SHA256_URL="https://dl.breadway.dev/bakery/latest/bakery-x86_64.sha256"
|
||||
SIG_URL="https://dl.breadway.dev/bakery/latest/bakery-x86_64.minisig"
|
||||
SIG_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/latest/download/bakery-x86_64.minisig"
|
||||
else
|
||||
# Strip a leading 'v' if the caller included it, then add it back consistently.
|
||||
ver="${BAKERY_VERSION#v}"
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/v${ver}/bakery-x86_64"
|
||||
SHA256_URL="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64.sha256"
|
||||
SIG_URL="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64.minisig"
|
||||
SIG_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/v${ver}/bakery-x86_64.minisig"
|
||||
fi
|
||||
|
||||
# Pick a download tool.
|
||||
|
|
@ -38,30 +49,63 @@ fi
|
|||
|
||||
mkdir -p "${BIN_DIR}"
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "${TMP}" "${TMP}.sha256"' EXIT
|
||||
trap 'rm -f "${TMP}" "${TMP}.sha256" "${TMP}.minisig"' EXIT
|
||||
|
||||
echo "downloading bakery…"
|
||||
if fetch "${DL_PRIMARY}" "${TMP}" 2>/dev/null; then
|
||||
echo " from dl.breadway.dev"
|
||||
# Verify checksum when available from primary.
|
||||
if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
||||
expected="$(awk '{print $1}' "${TMP}.sha256")"
|
||||
actual="$(sha256sum "${TMP}" | awk '{print $1}')"
|
||||
if [ "${expected}" != "${actual}" ]; then
|
||||
die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})"
|
||||
fi
|
||||
echo " checksum verified"
|
||||
else
|
||||
echo " warning: could not fetch checksum — skipping verification"
|
||||
fi
|
||||
sig_url="${SIG_URL}"
|
||||
checksum_only_fallback_note=" warning: could not fetch checksum — skipping verification"
|
||||
elif fetch "${DL_FALLBACK}" "${TMP}" 2>/dev/null; then
|
||||
echo " from GitHub (fallback)"
|
||||
# No .sha256 on the GitHub fallback path; proceed without verification.
|
||||
echo " warning: checksum not verified for GitHub fallback download"
|
||||
sig_url="${SIG_FALLBACK}"
|
||||
checksum_only_fallback_note=" warning: no checksum available for GitHub fallback download"
|
||||
else
|
||||
die "failed to download bakery from both primary and fallback URLs"
|
||||
fi
|
||||
|
||||
# Signature verification is the authoritative check: it proves the binary
|
||||
# was produced by whoever holds the bakery signing key, not just that bytes
|
||||
# match whatever the same (possibly compromised) server also reports as the
|
||||
# checksum. Prefer it whenever both a .minisig is published and a minisign
|
||||
# verifier is available on this machine.
|
||||
sig_verified=0
|
||||
if fetch "${sig_url}" "${TMP}.minisig" 2>/dev/null; then
|
||||
if command -v minisign >/dev/null 2>&1; then
|
||||
if minisign -V -q -m "${TMP}" -x "${TMP}.minisig" -P "${BAKERY_MINISIGN_PUBKEY}"; then
|
||||
echo " signature verified (minisign)"
|
||||
sig_verified=1
|
||||
else
|
||||
die "minisign signature verification FAILED — refusing to install a binary that doesn't match the pinned bakery key"
|
||||
fi
|
||||
else
|
||||
echo " warning: 'minisign' is not installed — cannot verify the binary's" >&2
|
||||
echo " warning: signature, only its checksum. Install minisign for the" >&2
|
||||
echo " warning: strongest guarantee: pacman -S minisign / apt install minisign" >&2
|
||||
fi
|
||||
else
|
||||
echo " warning: no .minisig published for this release yet — signature not verified" >&2
|
||||
fi
|
||||
|
||||
# Checksum is a secondary, best-effort check (kept for defense in depth and
|
||||
# for the case where minisign isn't installed). It is not a substitute for
|
||||
# signature verification: both the binary and its checksum typically come
|
||||
# from the same server, so a compromised server can serve a matching pair.
|
||||
if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
||||
expected="$(awk '{print $1}' "${TMP}.sha256")"
|
||||
actual="$(sha256sum "${TMP}" | awk '{print $1}')"
|
||||
if [ "${expected}" != "${actual}" ]; then
|
||||
die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})"
|
||||
fi
|
||||
echo " checksum verified"
|
||||
else
|
||||
echo "${checksum_only_fallback_note}"
|
||||
fi
|
||||
|
||||
if [ "${sig_verified}" -ne 1 ]; then
|
||||
echo " warning: proceeding WITHOUT a verified signature on the bakery binary" >&2
|
||||
fi
|
||||
|
||||
chmod +x "${TMP}"
|
||||
cp "${TMP}" "${BIN_DIR}/bakery"
|
||||
echo "installed bakery to ${BIN_DIR}/bakery"
|
||||
|
|
|
|||
|
|
@ -110,4 +110,12 @@ check "post_install[0]" \
|
|||
"echo installed" \
|
||||
"$(jq -r '.packages.fakepkg.post_install[0]' "${OUT}")"
|
||||
|
||||
check "services[0].sha256" \
|
||||
"$(sha256sum "${PKG_VER_DIR}/fakepkg.service" | awk '{print $1}')" \
|
||||
"$(jq -r '.packages.fakepkg.services[0].sha256' "${OUT}")"
|
||||
|
||||
check "config.example_sha256" \
|
||||
"$(sha256sum "${PKG_VER_DIR}/fakepkg.example.toml" | awk '{print $1}')" \
|
||||
"$(jq -r '.packages.fakepkg.config.example_sha256' "${OUT}")"
|
||||
|
||||
echo "OK: all gen-index assertions passed"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue