archlinux_inputs_fsck/
fsck.rs1use 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 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}