Files
SashegDev 874b6f20c2 Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
2026-10-29 22:04:28 +03:00

183 lines
5.9 KiB
Rust

//! The release steps as release.yml runs them: `sums`, tools/ci/sign_release.sh (OpenSSL), `verify`.
use horizon_release_sign::{SIGNATURE, SUMS, SigningKey, trusted_keys};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const TAG: &str = "v2026.10.19";
fn folder(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"horizon-release-sign-{name}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("horizon-linux-x86_64.tar.gz"), b"linux build").unwrap();
std::fs::write(dir.join("horizon-windows-x64.zip"), b"windows build").unwrap();
let sums = horizon_release_sign::sums_file(&[
(
"horizon-linux-x86_64.tar.gz".into(),
horizon_release_sign::sha256_hex(b"linux build"),
),
(
"horizon-windows-x64.zip".into(),
horizon_release_sign::sha256_hex(b"windows build"),
),
]);
std::fs::write(dir.join(SUMS), sums).unwrap();
dir
}
fn tool(args: &[&str], paths: &[&Path]) -> Output {
Command::new(env!("CARGO_BIN_EXE_horizon-release-sign"))
.args(args)
.args(paths)
.output()
.unwrap()
}
fn verify(dir: &Path, extra: &[&str]) -> Output {
let mut args = vec!["verify", "--tag", TAG];
args.extend(extra);
tool(&args, &[dir])
}
/// An OpenSSL 3 (the runner's), or None on a machine without one (macOS ships LibreSSL).
fn openssl() -> Option<String> {
[
"openssl",
"/opt/homebrew/opt/openssl@3/bin/openssl",
"/usr/local/opt/openssl@3/bin/openssl",
]
.into_iter()
.find(|o| {
Command::new(o)
.arg("version")
.output()
.is_ok_and(|v| v.stdout.starts_with(b"OpenSSL 3"))
})
.map(String::from)
}
fn sign_script(dir: &Path, env: &[(&str, &str)]) -> Output {
let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tools/ci/sign_release.sh");
let mut cmd = Command::new("bash");
cmd.arg(script)
.arg(TAG)
.arg(dir.join("horizon-linux-x86_64.tar.gz"))
.arg(dir.join("horizon-windows-x64.zip"))
.env_remove("RELEASE_SIGNING_KEY")
.env_remove("RELEASE_SIGNING_KEY_NEXT");
cmd.env("OPENSSL", openssl().unwrap());
for (k, v) in env {
cmd.env(k, v);
}
cmd.output().unwrap()
}
#[test]
fn a_signature_made_like_openssls_checks_and_tampering_fails() {
let dir = folder("lib");
let (key, _) = SigningKey::generate().unwrap();
let sums = std::fs::read(dir.join(SUMS)).unwrap();
std::fs::write(dir.join(SIGNATURE), key.sign(TAG, &sums) + "\n").unwrap();
assert!(
verify(&dir, &["--key", &key.public_base64()])
.status
.success()
);
std::fs::write(dir.join("horizon-windows-x64.zip"), b"tampered").unwrap();
let out = verify(&dir, &["--key", &key.public_base64()]);
assert!(!out.status.success());
assert!(String::from_utf8_lossy(&out.stderr).contains("doesn't match its signed checksum"));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn unsigned_releases_go_out_only_until_a_key_is_listed() {
let dir = folder("unsigned");
assert_eq!(
verify(&dir, &["--publishing"]).status.success(),
trusted_keys().is_empty()
);
assert!(!verify(&dir, &[]).status.success());
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn the_openssl_script_signs_what_the_launcher_checks() {
if openssl().is_none() {
eprintln!("no OpenSSL 3 here: tools/ci/sign_release.sh is tested on Linux");
return;
}
// A dry run: a throwaway key, its public key for the check.
let dir = folder("ephemeral");
std::fs::remove_file(dir.join(SUMS)).unwrap(); // the script writes its own
let out = sign_script(&dir, &[("EPHEMERAL", "yes")]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let key = std::fs::read_to_string(dir.join("ephemeral.pub")).unwrap();
assert!(verify(&dir, &["--key", key.trim()]).status.success());
std::fs::remove_dir_all(dir).unwrap();
// A release: both secrets (rotating), as keygen makes them; each signature line checks alone.
let dir = folder("secret");
let (old, old_secret) = SigningKey::generate().unwrap();
let (new, new_secret) = SigningKey::generate().unwrap();
let out = sign_script(
&dir,
&[
("RELEASE_SIGNING_KEY", &old_secret),
("RELEASE_SIGNING_KEY_NEXT", &new_secret),
],
);
let printed = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(out.status.success(), "{printed}");
assert!(!printed.contains(&old_secret) && !printed.contains(&new_secret));
assert_eq!(
std::fs::read_to_string(dir.join(SIGNATURE))
.unwrap()
.lines()
.count(),
2
);
assert!(
verify(&dir, &["--key", &old.public_base64()])
.status
.success()
);
assert!(
verify(&dir, &["--key", &new.public_base64()])
.status
.success()
);
let (other, _) = SigningKey::generate().unwrap();
assert!(
!verify(&dir, &["--key", &other.public_base64()])
.status
.success()
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn keygen_writes_the_secret_to_a_pipe_and_the_public_key_to_a_file() {
let dir = folder("keygen");
let public = dir.join("key.pub");
let out = tool(&["keygen", "--public-out"], &[&public]);
assert!(out.status.success());
let key = SigningKey::from_base64(&String::from_utf8(out.stdout).unwrap()).unwrap();
assert_eq!(
std::fs::read_to_string(&public).unwrap().trim(),
key.public_base64()
);
std::fs::remove_dir_all(dir).unwrap();
}