archlinux_inputs_fsck/
fsck.rs

1use crate::asp;
2use crate::errors::*;
3use crate::makepkg;
4use crate::makepkg::Source;
5use std::path::PathBuf;
6use std::str::FromStr;
7
8enum WorkDir {
9    Random(tempfile::TempDir),
10    Explicit(PathBuf),
11}
12
13#[derive(Debug, PartialEq)]
14enum AuthedSource {
15    File(String),
16    Url(UrlSource),
17    Git(GitSource),
18}
19
20impl AuthedSource {
21    fn url(s: Source) -> AuthedSource {
22        AuthedSource::Url(UrlSource {
23            url: s.url().to_string(),
24            filename: s.filename().map(String::from),
25            checksums: Vec::new(),
26        })
27    }
28}
29
30#[derive(Debug, PartialEq)]
31struct UrlSource {
32    url: String,
33    filename: Option<String>,
34    checksums: Vec<Checksum>,
35}
36
37impl UrlSource {
38    fn is_signature_file(&self) -> bool {
39        let filename = if let Some(filename) = &self.filename {
40            filename
41        } else {
42            &self.url
43        };
44
45        for ext in [".sig", ".asc", ".sign"] {
46            if filename.ends_with(ext) {
47                return true;
48            }
49        }
50
51        false
52    }
53}
54
55#[derive(Debug, PartialEq)]
56enum Checksum {
57    Md5(String),
58    Sha1(String),
59    Sha256(String),
60    Sha512(String),
61    B2(String),
62}
63
64impl Checksum {
65    fn new(alg: &str, value: String) -> Result<Checksum> {
66        Ok(match alg {
67            "md5sums" => Checksum::Md5(value),
68            "sha1sums" => Checksum::Sha1(value),
69            "sha256sums" => Checksum::Sha256(value),
70            "sha512sums" => Checksum::Sha512(value),
71            "b2sums" => Checksum::B2(value),
72            _ => bail!("Unknown checksum algorithm: {:?}", alg),
73        })
74    }
75}
76
77impl Checksum {
78    fn is_checksum_securely_pinned(&self) -> bool {
79        match self {
80            Checksum::Md5(_) => false,
81            Checksum::Sha1(_) => false,
82            Checksum::Sha256(_) => true,
83            Checksum::Sha512(_) => true,
84            Checksum::B2(_) => true,
85        }
86    }
87}
88
89#[derive(Debug, PartialEq)]
90struct GitSource {
91    url: String,
92    commit: Option<String>,
93    tag: Option<String>,
94    signed: bool,
95}
96
97impl GitSource {
98    fn is_commit_securely_pinned(&self) -> bool {
99        if let Some(commit) = &self.commit {
100            commit.len() == 40
101        } else {
102            false
103        }
104    }
105}
106
107impl FromStr for GitSource {
108    type Err = Error;
109
110    fn from_str(mut s: &str) -> Result<GitSource> {
111        let mut signed = false;
112        let mut commit = None;
113        let mut tag = None;
114
115        if let Some((remaining, value)) = s.rsplit_once("#commit=") {
116            commit = Some(value.to_string());
117            s = remaining;
118        }
119
120        if let Some((remaining, value)) = s.rsplit_once("#tag=") {
121            tag = Some(value.to_string());
122            s = remaining;
123        }
124
125        if let Some(remaining) = s.strip_suffix("?signed") {
126            signed = true;
127            s = remaining;
128        }
129
130        Ok(GitSource {
131            url: s.to_string(),
132            commit,
133            tag,
134            signed,
135        })
136    }
137}
138
139pub async fn check_pkg(pkg: &str, work_dir: Option<PathBuf>) -> Result<()> {
140    let work_dir = if let Some(work_dir) = &work_dir {
141        WorkDir::Explicit(work_dir.clone())
142    } else {
143        let tmp = tempfile::Builder::new()
144            .prefix("archlinux-inputs-fsck")
145            .tempdir()?;
146        WorkDir::Random(tmp)
147    };
148
149    let path = match &work_dir {
150        WorkDir::Explicit(root) => {
151            let mut path = root.clone();
152            path.push(pkg);
153            path.push("trunk");
154            path
155        }
156        WorkDir::Random(tmp) => {
157            let mut path = asp::checkout_package(pkg, tmp.path()).await?;
158            path.push("trunk");
159            path
160        }
161    };
162
163    let sources = makepkg::list_sources(&path).await?;
164    debug!("Found sources: {:?}", sources);
165
166    let mut findings = Vec::new();
167
168    let mut sources = sources
169        .into_iter()
170        .map(|source| {
171            let scheme = source.scheme();
172            Ok(match &scheme {
173                Some("https") => AuthedSource::url(source),
174                Some("http") => AuthedSource::url(source),
175                Some("ftp") => AuthedSource::url(source),
176                Some(scheme) if scheme.starts_with("git") => {
177                    if let "git" | "git+http" = *scheme {
178                        findings.push(format!("Using insecure {}:// scheme: {:?}", scheme, source));
179                        findings.push(format!("Using insecure {}:// scheme: {:?}", scheme, source));
180                    }
181
182                    AuthedSource::Git(source.url().parse()?)
183                }
184                Some("svn+https") => {
185                    findings.push(format!("Insecure svn+https:// scheme: {:?}", source));
186                    AuthedSource::url(source)
187                }
188                Some(scheme) => {
189                    findings.push(format!("Unknown scheme: {:?}", scheme));
190                    AuthedSource::url(source)
191                }
192                None => AuthedSource::File(source.url().to_string()),
193            })
194        })
195        .collect::<Result<Vec<_>>>()?;
196
197    for alg in makepkg::SUPPORTED_ALGS {
198        let sums = makepkg::list_variable(&path, alg).await?;
199        if sums.is_empty() {
200            continue;
201        }
202
203        debug!("Found checksums ({}): {:?}", alg, sums);
204
205        if sources.len() != sums.len() {
206            findings.push(format!(
207                "Number of checksums doesn't match number of sources (sources={}, {}={})",
208                sources.len(),
209                alg,
210                sums.len()
211            ));
212        }
213
214        for (i, sum) in sums.into_iter().enumerate() {
215            if sum == "SKIP" {
216                continue;
217            }
218
219            let cm = Checksum::new(alg, sum)?;
220            debug!("Found checksum for #{}: {:?}", i, cm);
221            if let AuthedSource::Url(source) = &mut sources[i] {
222                source.checksums.push(cm);
223            }
224        }
225    }
226
227    // if an upstream project has submodules it's normal for them to be listed
228    // in source= without pinning them by commit. As long as the primary repo
229    // is securely pinned it's fine, but there's no reliable way to determine which
230    // one is the primary one. So we just assume if any is pinned it's a-okay.
231    let has_any_secure_git_sources = sources.iter().any(|source| match source {
232        AuthedSource::Git(source) => source.is_commit_securely_pinned(),
233        _ => false,
234    });
235
236    for source in sources {
237        debug!("source={:?}", source);
238        match source {
239            AuthedSource::File(_) => (),
240            AuthedSource::Url(source) => {
241                if source.is_signature_file() {
242                    debug!("Skipping signature file: {:?}", source);
243                    continue;
244                }
245
246                if !source
247                    .checksums
248                    .iter()
249                    .any(|x| x.is_checksum_securely_pinned())
250                {
251                    findings.push(format!(
252                        "Url artifact is not securely pinned by checksums: {:?}",
253                        source
254                    ));
255                }
256            }
257            AuthedSource::Git(source) => {
258                if !has_any_secure_git_sources && !source.is_commit_securely_pinned() {
259                    findings.push(format!("Git commit is not securely pinned: {:?}", source));
260                }
261            }
262        }
263    }
264
265    let validpgpkeys = makepkg::list_variable(&path, "validpgpkeys").await?;
266    if !validpgpkeys.is_empty() {
267        debug!("Found validpgpkeys={:?}", validpgpkeys);
268    }
269
270    for finding in findings {
271        warn!("{:?}: {}", pkg, finding);
272    }
273
274    Ok(())
275}