Support signature verification for mise and syft

This commit is contained in:
Taiki Endo
2026-03-22 01:45:04 +09:00
parent 525387f706
commit c35d18270e
9 changed files with 298 additions and 35 deletions

View File

@@ -20,6 +20,7 @@ libicu
linkcheck
mdbook
microdnf
minisig
mirrorlist
nextest
pluginconf
@@ -30,6 +31,8 @@ rclone
rdme
rootfs
sccache
SHASUMS
sigstore
syft
tombi
udeps

View File

@@ -10,6 +10,12 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
## [Unreleased]
- Support signature verification for `mise` and `syft`. ([#1611](https://github.com/taiki-e/install-action/pull/1611))
- Update `mise@latest` to 2026.3.10.
- Update `knope@latest` to 0.22.4.
- Update `cargo-binstall@latest` to 1.17.8.
- Update `tombi@latest` to 0.9.9.

36
manifests/knope.json generated
View File

@@ -3,10 +3,42 @@
"template": null,
"license_markdown": "[MIT](https://github.com/knope-dev/knope/blob/main/LICENSE)",
"latest": {
"version": "0.22.3"
"version": "0.22.4"
},
"0.22": {
"version": "0.22.3"
"version": "0.22.4"
},
"0.22.4": {
"x86_64_linux_musl": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-x86_64-unknown-linux-musl.tgz",
"etag": "0x8DE8761D8F513DE",
"hash": "45a74925ae9f4c9c2c33b51992ae50241ec4fa836bf8d2977c0b8e8172dd69cf",
"bin": "knope-x86_64-unknown-linux-musl/knope"
},
"x86_64_macos": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-x86_64-apple-darwin.tgz",
"etag": "0x8DE8761D8E4D27D",
"hash": "010dc197bf159bbd9d60e897252248ba2b0e204beae7250ce54a9deae1ec4876",
"bin": "knope-x86_64-apple-darwin/knope"
},
"x86_64_windows": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-x86_64-pc-windows-msvc.tgz",
"etag": "0x8DE8761D8EAE61C",
"hash": "09f735b2da42cd594189042d1379c0a3a350a8c0ccb741015a84c6ff334543b1",
"bin": "knope-x86_64-pc-windows-msvc/knope.exe"
},
"aarch64_linux_musl": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-aarch64-unknown-linux-musl.tgz",
"etag": "0x8DE8761D8EE649C",
"hash": "95e882afdb4154c5baaba91f7bbd1fb1d41cec6898363a2b30e7abad4057b83b",
"bin": "knope-aarch64-unknown-linux-musl/knope"
},
"aarch64_macos": {
"url": "https://github.com/knope-dev/knope/releases/download/knope/v0.22.4/knope-aarch64-apple-darwin.tgz",
"etag": "0x8DE8761D8EC1D47",
"hash": "02131f284315c8ece8a4ef69a0aff5f658309d4df73b95cfdfbe0fbd9e9ce259",
"bin": "knope-aarch64-apple-darwin/knope"
}
},
"0.22.3": {
"x86_64_linux_musl": {

32
manifests/mise.json generated
View File

@@ -28,13 +28,39 @@
},
"license_markdown": "[MIT](https://github.com/jdx/mise/blob/main/LICENSE)",
"latest": {
"version": "2026.3.9"
"version": "2026.3.10"
},
"2026": {
"version": "2026.3.9"
"version": "2026.3.10"
},
"2026.3": {
"version": "2026.3.9"
"version": "2026.3.10"
},
"2026.3.10": {
"x86_64_linux_musl": {
"etag": "0x8DE8749F9B8C65D",
"hash": "b0c2fd25fe95cd1ed3f178b95690608472aa8a27ce2f6e63eaebda52a238e570"
},
"x86_64_macos": {
"etag": "0x8DE8749FBD637A3",
"hash": "5ed1a2a6a79aab33e67d21156ec42b22d3cde1ceef09eb08c0ccd9b429795e6a"
},
"x86_64_windows": {
"etag": "0x8DE8749FC98ACBC",
"hash": "bf5e86077f652caca0413155e33886c3459d3f2963f9f186be76c8c05c2accb6"
},
"aarch64_linux_musl": {
"etag": "0x8DE8749F6470833",
"hash": "9730abf52c93c7945f907f4fe6f731b79d74671705656fa36fa45008933e88c7"
},
"aarch64_macos": {
"etag": "0x8DE8749FB48C7AB",
"hash": "85b5e577a5ed34431718091122ea7ec9cf7d4e1d8e5e4dc298cdb02d8dbd97b3"
},
"aarch64_windows": {
"etag": "0x8DE8749FC8C37A5",
"hash": "82a702481c9e877b28f82eb60a0d3be2d393fc9b7915283992b1cd0263724d2b"
}
},
"2026.3.9": {
"x86_64_linux_musl": {

View File

@@ -4,6 +4,9 @@
"rust_crate": "${package}",
"bin": "mise/bin/${package}${exe}",
"version_range": ">= 2025.9.7",
"signing": {
"kind": "custom"
},
"platform": {
"x86_64_linux_musl": {
"asset_name": "${package}-v${version}-${rust_target_os}-x64-musl.tar.gz"

View File

@@ -3,6 +3,10 @@
"tag_prefix": "v",
"bin": "${package}${exe}",
"version_range": ">= 0.83.0",
"signing": {
"version_range": ">= 0.104.0",
"kind": "custom"
},
"platform": {
"x86_64_linux_musl": {
"asset_name": "${package}_${version}_linux_amd64.tar.gz"

View File

@@ -95,6 +95,8 @@ pub enum SigningKind {
/// public key: package.metadata.binstall.signing.pubkey at Cargo.toml
/// <https://github.com/cargo-bins/cargo-binstall/blob/HEAD/SIGNING.md>
MinisignBinstall,
/// tool-specific
Custom,
}
#[derive(Debug, Deserialize)]

View File

@@ -9,7 +9,7 @@ use std::{
env,
ffi::OsStr,
io::Read as _,
path::Path,
path::{Path, PathBuf},
sync::{LazyLock, RwLock},
time::Duration,
};
@@ -20,6 +20,7 @@ use install_action_internal_codegen::{
BaseManifest, HostPlatform, Manifest, ManifestDownloadInfo, ManifestRef, ManifestTemplate,
ManifestTemplateDownloadInfo, Manifests, SigningKind, Version, workspace_root,
};
use serde::de::DeserializeOwned;
use spdx::expression::{ExprNode, ExpressionReq, Operator};
fn main() {
@@ -30,7 +31,7 @@ fn main() {
);
std::process::exit(1);
}
let package = &args[0];
let package = &*args[0];
let version_req = args.get(1);
let version_req_given = version_req.is_some();
let skip_existing_manifest_versions = std::env::var("SKIP_EXISTING_MANIFEST_VERSIONS").is_ok();
@@ -56,8 +57,7 @@ fn main() {
.unwrap();
eprintln!("downloading metadata from {GITHUB_API_START}repos/{repo}");
let repo_info: github::RepoMetadata =
download(&format!("{GITHUB_API_START}repos/{repo}")).unwrap().into_json().unwrap();
let repo_info: github::RepoMetadata = download_json(&format!("{GITHUB_API_START}repos/{repo}"));
eprintln!("downloading releases from {GITHUB_API_START}repos/{repo}/releases");
let mut releases: github::Releases = vec![];
@@ -65,12 +65,9 @@ fn main() {
// is greater than 100, multiple fetches are needed.
for page in 1.. {
let per_page = 100;
let mut r: github::Releases = download(&format!(
let mut r: github::Releases = download_json(&format!(
"{GITHUB_API_START}repos/{repo}/releases?per_page={per_page}&page={page}"
))
.unwrap()
.into_json()
.unwrap();
));
// If version_req is latest, it is usually sufficient to look at the latest 100 releases.
if r.len() < per_page || version_req.is_some_and(|req| req == "latest") {
releases.append(&mut r);
@@ -112,16 +109,13 @@ fn main() {
.unwrap();
if let Some(crate_name) = &base_info.rust_crate {
eprintln!("downloading crate info from https://crates.io/api/v1/crates/{crate_name}");
let info = download(&format!("https://crates.io/api/v1/crates/{crate_name}"))
.unwrap()
.into_json::<crates_io::Crate>()
.unwrap();
let info: crates_io::Crate =
download_json(&format!("https://crates.io/api/v1/crates/{crate_name}"));
let latest_version = &info.versions[0].num;
crates_io_version_detail = Some(
download(&format!("https://crates.io/api/v1/crates/{crate_name}/{latest_version}"))
.unwrap()
.into_json::<crates_io::VersionMetadata>()
.unwrap()
download_json::<crates_io::VersionMetadata>(&format!(
"https://crates.io/api/v1/crates/{crate_name}/{latest_version}"
))
.version,
);
@@ -261,6 +255,14 @@ fn main() {
}
};
let signing_version_req: Option<semver::VersionReq> =
base_info.signing.as_ref().map(|signing| {
match &signing.version_range {
Some(version_range) => version_range.parse().unwrap(),
None => ">= 0.0.1".parse().unwrap(), // HACK: ignore pre-releases
}
});
let mut buf = vec![];
let mut buf2 = vec![];
for (Reverse(semver_version), (version, release)) in &releases {
@@ -282,11 +284,154 @@ fn main() {
continue;
}
let signing_version_req: Option<semver::VersionReq> = match &base_info.signing {
let mut verified_checksum: Option<Vec<_>> = None;
match &base_info.signing {
Some(signing) => {
match &signing.version_range {
Some(version_range) => Some(version_range.parse().unwrap()),
None => Some(">= 0.0.1".parse().unwrap()), // HACK: ignore pre-releases
if let SigningKind::Custom = signing.kind {
match package {
_ if !signing_version_req.as_ref().unwrap().matches(semver_version) => {}
"mise" => {
// Refs: https://github.com/jdx/mise/blob/v2026.3.9/src/minisign.rs
let crates_io_info = crates_io_info.as_ref().unwrap();
let [checksum, sig] =
["SHASUMS256.txt", "SHASUMS256.txt.minisig"].map(|f| {
let Some(asset) =
release.assets.iter().find(|asset| asset.name == f)
else {
// There is broken release which has no release assets: https://github.com/jdx/mise/releases/tag/v2026.2.14
return PathBuf::new();
};
let download_cache =
download_cache_dir.join(format!("{version}-{f}"));
let url = &asset.browser_download_url;
eprint!("downloading {url} for signature verification ... ");
if download_cache.is_file() {
eprintln!("already downloaded");
} else {
download_to_buf(url, &mut buf);
eprintln!("download complete");
fs::write(&download_cache, &buf).unwrap();
buf.clear();
}
download_cache
});
if checksum.as_os_str().is_empty() || sig.as_os_str().is_empty() {
continue;
}
let v = crates_io_info
.versions
.iter()
.find(|v| v.num == *semver_version)
.unwrap();
let url = format!("https://crates.io{}", v.dl_path);
let pubkey_download_cache =
&download_cache_dir.join(format!("{version}-minisign.pub"));
eprint!("downloading {url} for signature verification ... ");
if pubkey_download_cache.is_file() {
eprintln!("already downloaded");
} else {
download_to_buf(&url, &mut buf);
let hash = ring::digest::digest(&ring::digest::SHA256, &buf);
if format!("{hash:?}").strip_prefix("SHA256:").unwrap()
!= v.checksum
{
panic!("checksum mismatch for {url}");
}
let decoder = flate2::read::GzDecoder::new(&*buf);
let mut archive = tar::Archive::new(decoder);
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let path = entry.path().unwrap();
if path.file_name() == Some(OsStr::new("minisign.pub")) {
entry.unpack(pubkey_download_cache).unwrap();
break;
}
}
buf.clear();
eprintln!("download complete");
}
let pubkey =
minisign_verify::PublicKey::from_file(pubkey_download_cache)
.unwrap();
eprint!("verifying checksum file for {package}@{version} ... ");
let allow_legacy = false;
pubkey
.verify(
&fs::read(&checksum).unwrap(),
&minisign_verify::Signature::from_file(sig).unwrap(),
allow_legacy,
)
.unwrap();
verified_checksum = Some(
fs::read_to_string(checksum)
.unwrap()
.lines()
.filter_map(|l| l.split_once(" "))
.map(|(h, f)| {
(f.trim_ascii().to_owned(), h.trim_ascii().to_owned())
})
.collect(),
);
eprintln!("done");
}
"syft" => {
// Refs: https://oss.anchore.com/docs/installation/verification/
let [checksum, certificate, signature] =
["checksums.txt", "checksums.txt.pem", "checksums.txt.sig"].map(
|f| {
let asset = release
.assets
.iter()
.find(|asset| asset.name.ends_with(f))
.unwrap();
let download_cache =
download_cache_dir.join(format!("{version}-{f}"));
let url = &asset.browser_download_url;
eprint!(
"downloading {url} for signature verification ... "
);
if download_cache.is_file() {
eprintln!("already downloaded");
} else {
download_to_buf(url, &mut buf);
eprintln!("download complete");
fs::write(&download_cache, &buf).unwrap();
buf.clear();
}
download_cache
},
);
eprint!("verifying checksum file for {package}@{version} ... ");
cmd!(
"cosign",
"verify-blob",
&checksum,
"--certificate",
certificate,
"--signature",
signature,
"--certificate-identity-regexp",
format!("https://github\\.com/{repo}/\\.github/workflows/.+"),
"--certificate-oidc-issuer",
"https://token.actions.githubusercontent.com"
)
.run()
.unwrap();
verified_checksum = Some(
fs::read_to_string(checksum)
.unwrap()
.lines()
.filter_map(|l| l.split_once(" "))
.map(|(h, f)| {
(f.trim_ascii().to_owned(), h.trim_ascii().to_owned())
})
.collect(),
);
eprintln!("done");
}
_ => {}
}
}
}
None => {
@@ -294,18 +439,22 @@ fn main() {
asset.name.contains(".asc")
|| asset.name.contains(".gpg")
|| asset.name.contains(".sig")
|| asset.name.contains(".minisig")
|| asset.name.contains(".pem")
|| asset.name.contains(".crt")
|| asset.name.contains(".key")
|| asset.name.contains(".pub")
}) {
eprintln!(
"{package} may supports other signing verification method using {}",
"{package} may supports other signature verification method using {}",
asset.name
);
}
None
}
};
}
let mut download_info = BTreeMap::new();
let mut pubkey = None;
let mut minisign_binstall_pubkey = None;
for (&platform, base_download_info) in &base_info.platform {
let asset_names = base_download_info
.asset_name
@@ -440,7 +589,7 @@ fn main() {
if crate_download_cache.is_file() {
eprintln!("already downloaded");
} else {
download(&url).unwrap().into_reader().read_to_end(&mut buf2).unwrap();
download_to_buf(&url, &mut buf2);
let hash = ring::digest::digest(&ring::digest::SHA256, &buf2);
if format!("{hash:?}").strip_prefix("SHA256:").unwrap() != v.checksum {
panic!("checksum mismatch for {url}");
@@ -458,7 +607,7 @@ fn main() {
buf2.clear();
eprintln!("download complete");
}
if pubkey.is_none() {
if minisign_binstall_pubkey.is_none() {
let cargo_manifest = toml::de::from_str::<cargo_manifest::Manifest>(
&fs::read_to_string(crate_download_cache).unwrap(),
)
@@ -475,19 +624,42 @@ fn main() {
cargo_manifest.package.metadata.binstall.signing.algorithm,
"minisign"
);
pubkey = Some(
minisign_binstall_pubkey = Some(
minisign_verify::PublicKey::from_base64(
&cargo_manifest.package.metadata.binstall.signing.pubkey,
)
.unwrap(),
);
}
let pubkey = pubkey.as_ref().unwrap();
let pubkey = minisign_binstall_pubkey.as_ref().unwrap();
eprint!("verifying signature for {bin_url} ... ");
let allow_legacy = false;
pubkey.verify(&buf, &sig, allow_legacy).unwrap();
eprintln!("done");
}
SigningKind::Custom => {
if let Some(verified_checksum) = &verified_checksum {
let asset_name_cwd = format!("./{asset_name}");
let mut checked = false;
for (f, h) in verified_checksum {
if *f == asset_name || *f == asset_name_cwd {
checked = true;
assert_eq!(
hash, *h,
"verified checksum doesn't match with sha256 hash of {asset_name} in {package}@{version}"
);
}
}
assert!(
checked,
"{asset_name} not found in verified checksum for {package}@{version}"
);
} else {
unimplemented!(
"unimplemented tool-specific signing handling for {package}"
);
}
}
}
}
@@ -863,6 +1035,16 @@ fn download(url: &str) -> Result<ureq::Response> {
Err(last_error.unwrap().into())
}
#[track_caller]
fn download_to_buf(url: &str, buf: &mut Vec<u8>) {
download(url).unwrap().into_reader().read_to_end(buf).unwrap();
}
#[track_caller]
fn download_json<T: DeserializeOwned>(url: &str) -> T {
download(url).unwrap().into_json().unwrap()
}
fn github_head(url: &str) -> Result<()> {
eprintln!("fetching head of {url} ..");
let mut token = GITHUB_TOKENS.get(url);

View File

@@ -11,6 +11,11 @@ cd -- "$(dirname -- "$0")"/..
# ./tools/manifest.sh [PACKAGE [VERSION_REQ]]
# ./tools/manifest.sh full
if [[ -n "${GITHUB_ACTIONS:-}" ]] && ! type -P cosign; then
go install github.com/sigstore/cosign/v3/cmd/cosign@latest
sudo mv -- ~/go/bin/cosign /usr/local/bin
fi
if [[ $# -eq 1 ]] && [[ "$1" == "full" ]]; then
for manifest in tools/codegen/base/*.json; do
package="${manifest##*/}"