- 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.
83 lines
2.4 KiB
Rust
83 lines
2.4 KiB
Rust
use anyhow::{bail, Context, Result};
|
|
use sha2::{Digest, Sha256};
|
|
use std::path::Path;
|
|
|
|
use crate::manifest::{fetch_binary, Binary};
|
|
|
|
/// Download a binary to a temp path, verify its SHA-256, then atomically move
|
|
/// it into place. Bails before touching `dest` if the checksum fails.
|
|
pub fn fetch_and_place(binary: &Binary, dest: &Path) -> Result<()> {
|
|
println!(" downloading {}…", binary.name);
|
|
let bytes = fetch_binary(&binary.dl_url, &binary.github_url)
|
|
.with_context(|| format!("downloading {}", binary.name))?;
|
|
|
|
verify_sha256(&bytes, &binary.sha256)
|
|
.with_context(|| format!("checksum mismatch for {}", binary.name))?;
|
|
|
|
if let Some(dir) = dest.parent() {
|
|
std::fs::create_dir_all(dir)?;
|
|
}
|
|
|
|
let tmp = dest.with_extension("tmp");
|
|
std::fs::write(&tmp, &bytes).context("writing binary to tmp")?;
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))?;
|
|
}
|
|
|
|
std::fs::rename(&tmp, dest).context("placing binary")?;
|
|
println!(" installed {}", dest.display());
|
|
Ok(())
|
|
}
|
|
|
|
/// Verify that `bytes` hashes to `expected_hex` under SHA-256.
|
|
///
|
|
/// Shared by every artifact download path — binaries (via
|
|
/// [`fetch_and_place`]), and config-example / systemd-unit downloads in
|
|
/// `install.rs` — so all downloaded artifacts get the same integrity check.
|
|
pub fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<()> {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(bytes);
|
|
let actual = hex::encode(hasher.finalize());
|
|
if actual != expected_hex {
|
|
bail!(
|
|
"SHA-256 mismatch\n expected: {}\n actual: {}",
|
|
expected_hex,
|
|
actual
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use sha2::{Digest, Sha256};
|
|
|
|
fn sha256_hex(data: &[u8]) -> String {
|
|
hex::encode(Sha256::digest(data))
|
|
}
|
|
|
|
#[test]
|
|
fn verify_correct_hash() {
|
|
let bytes = b"hello bakery";
|
|
let hash = sha256_hex(bytes);
|
|
assert!(verify_sha256(bytes, &hash).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn verify_wrong_hash_fails() {
|
|
let bytes = b"hello bakery";
|
|
let wrong = "0".repeat(64);
|
|
assert!(verify_sha256(bytes, &wrong).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn verify_empty_bytes() {
|
|
let bytes = b"";
|
|
let hash = sha256_hex(bytes);
|
|
assert!(verify_sha256(bytes, &hash).is_ok());
|
|
}
|
|
}
|