use crate::asp;
use crate::errors::*;
use crate::makepkg;
use crate::makepkg::Source;
use std::path::PathBuf;
use std::str::FromStr;
enum WorkDir {
Random(tempfile::TempDir),
Explicit(PathBuf),
}
#[derive(Debug, PartialEq)]
enum AuthedSource {
File(String),
Url(UrlSource),
Git(GitSource),
}
impl AuthedSource {
fn url(s: Source) -> AuthedSource {
AuthedSource::Url(UrlSource {
url: s.url().to_string(),
filename: s.filename().map(String::from),
checksums: Vec::new(),
})
}
}
#[derive(Debug, PartialEq)]
struct UrlSource {
url: String,
filename: Option<String>,
checksums: Vec<Checksum>,
}
impl UrlSource {
fn is_signature_file(&self) -> bool {
let filename = if let Some(filename) = &self.filename {
filename
} else {
&self.url
};
for ext in [".sig", ".asc", ".sign"] {
if filename.ends_with(ext) {
return true;
}
}
false
}
}
#[derive(Debug, PartialEq)]
enum Checksum {
Md5(String),
Sha1(String),
Sha256(String),
Sha512(String),
B2(String),
}
impl Checksum {
fn new(alg: &str, value: String) -> Result<Checksum> {
Ok(match alg {
"md5sums" => Checksum::Md5(value),
"sha1sums" => Checksum::Sha1(value),
"sha256sums" => Checksum::Sha256(value),
"sha512sums" => Checksum::Sha512(value),
"b2sums" => Checksum::B2(value),
_ => bail!("Unknown checksum algorithm: {:?}", alg),
})
}
}
impl Checksum {
fn is_checksum_securely_pinned(&self) -> bool {
match self {
Checksum::Md5(_) => false,
Checksum::Sha1(_) => false,
Checksum::Sha256(_) => true,
Checksum::Sha512(_) => true,
Checksum::B2(_) => true,
}
}
}
#[derive(Debug, PartialEq)]
struct GitSource {
url: String,
commit: Option<String>,
tag: Option<String>,
signed: bool,
}
impl GitSource {
fn is_commit_securely_pinned(&self) -> bool {
if let Some(commit) = &self.commit {
commit.len() == 40
} else {
false
}
}
}
impl FromStr for GitSource {
type Err = Error;
fn from_str(mut s: &str) -> Result<GitSource> {
let mut signed = false;
let mut commit = None;
let mut tag = None;
if let Some((remaining, value)) = s.rsplit_once("#commit=") {
commit = Some(value.to_string());
s = remaining;
}
if let Some((remaining, value)) = s.rsplit_once("#tag=") {
tag = Some(value.to_string());
s = remaining;
}
if let Some(remaining) = s.strip_suffix("?signed") {
signed = true;
s = remaining;
}
Ok(GitSource {
url: s.to_string(),
commit,
tag,
signed,
})
}
}
pub async fn check_pkg(pkg: &str, work_dir: Option<PathBuf>) -> Result<()> {
let work_dir = if let Some(work_dir) = &work_dir {
WorkDir::Explicit(work_dir.clone())
} else {
let tmp = tempfile::Builder::new()
.prefix("archlinux-inputs-fsck")
.tempdir()?;
WorkDir::Random(tmp)
};
let path = match &work_dir {
WorkDir::Explicit(root) => {
let mut path = root.clone();
path.push(pkg);
path.push("trunk");
path
}
WorkDir::Random(tmp) => {
let mut path = asp::checkout_package(pkg, tmp.path()).await?;
path.push("trunk");
path
}
};
let sources = makepkg::list_sources(&path).await?;
debug!("Found sources: {:?}", sources);
let mut findings = Vec::new();
let mut sources = sources
.into_iter()
.map(|source| {
let scheme = source.scheme();
Ok(match &scheme {
Some("https") => AuthedSource::url(source),
Some("http") => AuthedSource::url(source),
Some("ftp") => AuthedSource::url(source),
Some(scheme) if scheme.starts_with("git") => {
if let "git" | "git+http" = *scheme {
findings.push(format!("Using insecure {}:// scheme: {:?}", scheme, source));
findings.push(format!("Using insecure {}:// scheme: {:?}", scheme, source));
}
AuthedSource::Git(source.url().parse()?)
}
Some("svn+https") => {
findings.push(format!("Insecure svn+https:// scheme: {:?}", source));
AuthedSource::url(source)
}
Some(scheme) => {
findings.push(format!("Unknown scheme: {:?}", scheme));
AuthedSource::url(source)
}
None => AuthedSource::File(source.url().to_string()),
})
})
.collect::<Result<Vec<_>>>()?;
for alg in makepkg::SUPPORTED_ALGS {
let sums = makepkg::list_variable(&path, alg).await?;
if sums.is_empty() {
continue;
}
debug!("Found checksums ({}): {:?}", alg, sums);
if sources.len() != sums.len() {
findings.push(format!(
"Number of checksums doesn't match number of sources (sources={}, {}={})",
sources.len(),
alg,
sums.len()
));
}
for (i, sum) in sums.into_iter().enumerate() {
if sum == "SKIP" {
continue;
}
let cm = Checksum::new(alg, sum)?;
debug!("Found checksum for #{}: {:?}", i, cm);
if let AuthedSource::Url(source) = &mut sources[i] {
source.checksums.push(cm);
}
}
}
let has_any_secure_git_sources = sources.iter().any(|source| match source {
AuthedSource::Git(source) => source.is_commit_securely_pinned(),
_ => false,
});
for source in sources {
debug!("source={:?}", source);
match source {
AuthedSource::File(_) => (),
AuthedSource::Url(source) => {
if source.is_signature_file() {
debug!("Skipping signature file: {:?}", source);
continue;
}
if !source
.checksums
.iter()
.any(|x| x.is_checksum_securely_pinned())
{
findings.push(format!(
"Url artifact is not securely pinned by checksums: {:?}",
source
));
}
}
AuthedSource::Git(source) => {
if !has_any_secure_git_sources && !source.is_commit_securely_pinned() {
findings.push(format!("Git commit is not securely pinned: {:?}", source));
}
}
}
}
let validpgpkeys = makepkg::list_variable(&path, "validpgpkeys").await?;
if !validpgpkeys.is_empty() {
debug!("Found validpgpkeys={:?}", validpgpkeys);
}
for finding in findings {
warn!("{:?}: {}", pkg, finding);
}
Ok(())
}