mirror of
https://github.com/taiki-e/install-action.git
synced 2026-04-21 15:10:27 +00:00
Support signature verification (minisign)
This commit is contained in:
1
.github/.cspell/project-dictionary.txt
vendored
1
.github/.cspell/project-dictionary.txt
vendored
@@ -14,6 +14,7 @@ mdbook
|
|||||||
microdnf
|
microdnf
|
||||||
nextest
|
nextest
|
||||||
protoc
|
protoc
|
||||||
|
pubkey
|
||||||
pwsh
|
pwsh
|
||||||
quickinstall
|
quickinstall
|
||||||
shellcheck
|
shellcheck
|
||||||
|
|||||||
2
.github/.cspell/rust-dependencies.txt
generated
vendored
2
.github/.cspell/rust-dependencies.txt
generated
vendored
@@ -1,4 +1,6 @@
|
|||||||
// This file is @generated by tidy.sh.
|
// This file is @generated by tidy.sh.
|
||||||
// It is not intended for manual editing.
|
// It is not intended for manual editing.
|
||||||
|
|
||||||
|
flate
|
||||||
|
minisign
|
||||||
ureq
|
ureq
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Note: In this file, do not use the hard wrap in the middle of a sentence for com
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Support signature verification. ([#237](https://github.com/taiki-e/install-action/pull/237))
|
||||||
|
|
||||||
- Update `syft@latest` to 0.92.0.
|
- Update `syft@latest` to 0.92.0.
|
||||||
|
|
||||||
- Update `cargo-make@latest` to 0.37.2.
|
- Update `cargo-make@latest` to 0.37.2.
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ When installing the tool from GitHub Releases, this action will download the too
|
|||||||
|
|
||||||
Additionally, this action will also verify SHA256 checksums for downloaded files in all tools installed from GitHub Releases. This is enabled by default and can be disabled by setting the `checksum` input option to `false`.
|
Additionally, this action will also verify SHA256 checksums for downloaded files in all tools installed from GitHub Releases. This is enabled by default and can be disabled by setting the `checksum` input option to `false`.
|
||||||
|
|
||||||
|
Additionally, we also verify signature if the tool distributes signed archives. Signature verification is done at the stage of getting the checksum, so disabling the checksum will also disable signature verification.
|
||||||
|
|
||||||
See the linked documentation for information on security when installed using [snap](https://snapcraft.io/docs) or [cargo-binstall](https://github.com/cargo-bins/cargo-binstall#faq).
|
See the linked documentation for information on security when installed using [snap](https://snapcraft.io/docs) or [cargo-binstall](https://github.com/cargo-bins/cargo-binstall#faq).
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
flate2 = "1"
|
||||||
fs-err = "2"
|
fs-err = "2"
|
||||||
|
minisign-verify = "0.2"
|
||||||
semver = { version = "1", features = ["serde"] }
|
semver = { version = "1", features = ["serde"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
tar = "0.4"
|
||||||
|
toml = "0.8"
|
||||||
ureq = { version = "2", features = ["json"] }
|
ureq = { version = "2", features = ["json"] }
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"rust_crate": "${package}",
|
"rust_crate": "${package}",
|
||||||
"asset_name": "${package}-${rust_target}.tgz",
|
"asset_name": "${package}-${rust_target}.tgz",
|
||||||
"version_range": "latest",
|
"version_range": "latest",
|
||||||
|
"signing": {
|
||||||
|
"kind": "minisign-binstall"
|
||||||
|
},
|
||||||
"platform": {
|
"platform": {
|
||||||
"x86_64_linux_musl": {},
|
"x86_64_linux_musl": {},
|
||||||
"x86_64_macos": {
|
"x86_64_macos": {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
|
|
||||||
|
#![allow(clippy::single_match)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Reverse,
|
cmp::Reverse,
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, BTreeSet},
|
||||||
env, fmt,
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
|
fmt,
|
||||||
io::Read,
|
io::Read,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
slice,
|
slice,
|
||||||
@@ -118,19 +122,6 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
let version_req: Option<semver::VersionReq> = match args.get(1) {
|
let version_req: Option<semver::VersionReq> = match args.get(1) {
|
||||||
_ if latest_only => {
|
_ if latest_only => {
|
||||||
if args.get(1).map(String::as_str) == Some("latest") {
|
|
||||||
if let Some(m) = manifests.map.first_key_value() {
|
|
||||||
let version = match m.1 {
|
|
||||||
ManifestRef::Ref { version } => version,
|
|
||||||
ManifestRef::Real(_) => &m.0 .0,
|
|
||||||
};
|
|
||||||
if !manifests.map.is_empty()
|
|
||||||
&& *version >= releases.first_key_value().unwrap().0 .0.clone().into()
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let req = format!("={}", releases.first_key_value().unwrap().0 .0).parse()?;
|
let req = format!("={}", releases.first_key_value().unwrap().0 .0).parse()?;
|
||||||
eprintln!("update manifest for versions '{req}'");
|
eprintln!("update manifest for versions '{req}'");
|
||||||
Some(req)
|
Some(req)
|
||||||
@@ -154,7 +145,7 @@ fn main() -> Result<()> {
|
|||||||
if manifests.map.is_empty() {
|
if manifests.map.is_empty() {
|
||||||
format!("={}", releases.first_key_value().unwrap().0 .0).parse()?
|
format!("={}", releases.first_key_value().unwrap().0 .0).parse()?
|
||||||
} else {
|
} else {
|
||||||
format!(">{}", semver_versions.last().unwrap()).parse()?
|
format!(">={}", semver_versions.last().unwrap()).parse()?
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
version_req.parse()?
|
version_req.parse()?
|
||||||
@@ -165,6 +156,7 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
|
let mut buf2 = vec![];
|
||||||
for (Reverse(semver_version), (version, release)) in &releases {
|
for (Reverse(semver_version), (version, release)) in &releases {
|
||||||
if let Some(version_req) = &version_req {
|
if let Some(version_req) = &version_req {
|
||||||
if !version_req.matches(semver_version) {
|
if !version_req.matches(semver_version) {
|
||||||
@@ -178,6 +170,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut download_info = BTreeMap::new();
|
let mut download_info = BTreeMap::new();
|
||||||
|
let mut pubkey = None;
|
||||||
for (&platform, base_download_info) in &base_info.platform {
|
for (&platform, base_download_info) in &base_info.platform {
|
||||||
let asset_names = base_download_info
|
let asset_names = base_download_info
|
||||||
.asset_name
|
.asset_name
|
||||||
@@ -204,23 +197,101 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eprintln!("downloading {url} for checksum...");
|
eprint!("downloading {url} for checksum ... ");
|
||||||
let download_cache = download_cache_dir.join(format!(
|
let download_cache = &download_cache_dir.join(format!(
|
||||||
"{version}-{platform:?}-{}",
|
"{version}-{platform:?}-{}",
|
||||||
Path::new(&url).file_name().unwrap().to_str().unwrap()
|
Path::new(&url).file_name().unwrap().to_str().unwrap()
|
||||||
));
|
));
|
||||||
if download_cache.is_file() {
|
if download_cache.is_file() {
|
||||||
eprintln!(" already downloaded");
|
eprintln!("already downloaded");
|
||||||
fs::File::open(download_cache)?.read_to_end(&mut buf)?;
|
fs::File::open(download_cache)?.read_to_end(&mut buf)?;
|
||||||
} else {
|
} else {
|
||||||
download(&url)?.into_reader().read_to_end(&mut buf)?;
|
download(&url)?.into_reader().read_to_end(&mut buf)?;
|
||||||
eprintln!(" download complete");
|
eprintln!("download complete");
|
||||||
fs::write(download_cache, &buf)?;
|
fs::write(download_cache, &buf)?;
|
||||||
}
|
}
|
||||||
eprintln!("getting sha256 hash for {url}");
|
eprintln!("getting sha256 hash for {url}");
|
||||||
let hash = Sha256::digest(&buf);
|
let hash = Sha256::digest(&buf);
|
||||||
let hash = format!("{hash:x}");
|
let hash = format!("{hash:x}");
|
||||||
eprintln!("{hash} *{asset_name}");
|
eprintln!("{hash} *{asset_name}");
|
||||||
|
let bin_url = &url;
|
||||||
|
|
||||||
|
match base_info.signing {
|
||||||
|
Some(Signing { kind: SigningKind::MinisignBinstall }) => {
|
||||||
|
let url = url.clone() + ".sig";
|
||||||
|
let sig_download_cache = &download_cache.with_extension(format!(
|
||||||
|
"{}.sig",
|
||||||
|
download_cache.extension().unwrap_or_default().to_str().unwrap()
|
||||||
|
));
|
||||||
|
eprint!("downloading {url} for signature validation ... ");
|
||||||
|
let sig = if sig_download_cache.is_file() {
|
||||||
|
eprintln!("already downloaded");
|
||||||
|
minisign_verify::Signature::from_file(sig_download_cache)?
|
||||||
|
} else {
|
||||||
|
let buf = download(&url)?.into_string()?;
|
||||||
|
eprintln!("download complete");
|
||||||
|
fs::write(sig_download_cache, &buf)?;
|
||||||
|
minisign_verify::Signature::decode(&buf)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(crates_io_info) = &crates_io_info else {
|
||||||
|
bail!("signing kind minisign-binstall is supported only for rust crate");
|
||||||
|
};
|
||||||
|
let v =
|
||||||
|
crates_io_info.versions.iter().find(|v| v.num == *semver_version).unwrap();
|
||||||
|
let url = format!("https://crates.io{}", v.dl_path);
|
||||||
|
let crate_download_cache =
|
||||||
|
&download_cache_dir.join(format!("{version}-Cargo.toml"));
|
||||||
|
eprint!("downloading {url} for signature verification ... ");
|
||||||
|
if crate_download_cache.is_file() {
|
||||||
|
eprintln!("already downloaded");
|
||||||
|
} else {
|
||||||
|
download(&url)?.into_reader().read_to_end(&mut buf2)?;
|
||||||
|
let hash = Sha256::digest(&buf2);
|
||||||
|
if format!("{hash:x}") != v.checksum {
|
||||||
|
bail!("checksum mismatch for {url}");
|
||||||
|
}
|
||||||
|
let decoder = flate2::read::GzDecoder::new(&*buf2);
|
||||||
|
let mut archive = tar::Archive::new(decoder);
|
||||||
|
for entry in archive.entries()? {
|
||||||
|
let mut entry = entry?;
|
||||||
|
let path = entry.path()?;
|
||||||
|
if path.file_name() == Some(OsStr::new("Cargo.toml")) {
|
||||||
|
entry.unpack(crate_download_cache)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf2.clear();
|
||||||
|
eprintln!("download complete");
|
||||||
|
}
|
||||||
|
if pubkey.is_none() {
|
||||||
|
let cargo_manifest = toml::from_str::<cargo_manifest::Manifest>(
|
||||||
|
&fs::read_to_string(crate_download_cache)?,
|
||||||
|
)?;
|
||||||
|
eprintln!(
|
||||||
|
"algorithm: {}",
|
||||||
|
cargo_manifest.package.metadata.binstall.signing.algorithm
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"pubkey: {}",
|
||||||
|
cargo_manifest.package.metadata.binstall.signing.pubkey
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cargo_manifest.package.metadata.binstall.signing.algorithm,
|
||||||
|
"minisign"
|
||||||
|
);
|
||||||
|
pubkey = Some(minisign_verify::PublicKey::from_base64(
|
||||||
|
&cargo_manifest.package.metadata.binstall.signing.pubkey,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
let pubkey = pubkey.as_ref().unwrap();
|
||||||
|
eprint!("verifying signature for {bin_url} ... ");
|
||||||
|
let allow_legacy = false;
|
||||||
|
pubkey.verify(&buf, &sig, allow_legacy)?;
|
||||||
|
eprintln!("done");
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
download_info.insert(platform, ManifestDownloadInfo {
|
download_info.insert(platform, ManifestDownloadInfo {
|
||||||
url: Some(url),
|
url: Some(url),
|
||||||
@@ -620,10 +691,27 @@ struct BaseManifest {
|
|||||||
asset_name: Option<StringOrArray>,
|
asset_name: Option<StringOrArray>,
|
||||||
/// Path to binary in archive. Default to `${tool}${exe}`.
|
/// Path to binary in archive. Default to `${tool}${exe}`.
|
||||||
bin: Option<String>,
|
bin: Option<String>,
|
||||||
|
signing: Option<Signing>,
|
||||||
platform: BTreeMap<HostPlatform, BaseManifestPlatformInfo>,
|
platform: BTreeMap<HostPlatform, BaseManifestPlatformInfo>,
|
||||||
version_range: Option<String>,
|
version_range: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct Signing {
|
||||||
|
kind: SigningKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
enum SigningKind {
|
||||||
|
/// algorithm: minisign
|
||||||
|
/// public key: package.metadata.binstall.signing.pubkey at Cargo.toml
|
||||||
|
/// <https://github.com/cargo-bins/cargo-binstall/blob/HEAD/SIGNING.md>
|
||||||
|
MinisignBinstall,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct BaseManifestPlatformInfo {
|
struct BaseManifestPlatformInfo {
|
||||||
@@ -743,7 +831,39 @@ mod crates_io {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Version {
|
pub struct Version {
|
||||||
|
pub checksum: String,
|
||||||
|
pub dl_path: String,
|
||||||
pub num: semver::Version,
|
pub num: semver::Version,
|
||||||
pub yanked: bool,
|
pub yanked: bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod cargo_manifest {
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Manifest {
|
||||||
|
pub package: Package,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Package {
|
||||||
|
pub metadata: Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Metadata {
|
||||||
|
pub binstall: Binstall,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct Binstall {
|
||||||
|
pub signing: BinstallSigning,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct BinstallSigning {
|
||||||
|
pub algorithm: String,
|
||||||
|
pub pubkey: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user