crev_lib/
lib.rs

1#![type_length_limit = "10709970"]
2#![allow(clippy::implicit_hasher)]
3#![allow(clippy::items_after_statements)]
4#![allow(clippy::manual_range_contains)]
5#![allow(clippy::missing_errors_doc)]
6#![allow(clippy::missing_panics_doc)]
7#![allow(clippy::redundant_closure_for_method_calls)]
8
9pub mod activity;
10pub mod id;
11pub mod local;
12pub mod proof;
13pub mod repo;
14pub mod staging;
15pub mod util;
16pub use crate::local::Local;
17pub use activity::{ReviewActivity, ReviewMode};
18use crev_data::{
19    self,
20    id::IdError,
21    proof::{
22        review::{self, Rating},
23        trust::TrustLevel,
24        CommonOps,
25    },
26    Digest, Id, RegistrySource, Version,
27};
28use crev_wot::PkgVersionReviewId;
29pub use crev_wot::TrustDistanceParams;
30use log::warn;
31use std::error::Error as _;
32use std::{
33    collections::{HashMap, HashSet},
34    fmt,
35    path::{Path, PathBuf},
36};
37
38/// Failures that can happen in this library
39#[derive(Debug, thiserror::Error)]
40pub enum Error {
41    /// Trying to init a directory that is already there
42    #[error("`{}` already exists", _0.display())]
43    PathAlreadyExists(Box<Path>),
44
45    /// There are manual modifications in the git repo. Commit or reset them?
46    #[error("Git repository is not in a clean state")]
47    GitRepositoryIsNotInACleanState,
48
49    /// Found data from the future. Your version of crev is too old.
50    #[error("Unsupported version {}", _0)]
51    UnsupportedVersion(i64),
52
53    /// Your crev-id changed unexpectedly
54    #[error("PubKey mismatch")]
55    PubKeyMismatch,
56
57    /// You need to make a crev Id to perform most operations
58    #[error("User config not-initialized. Use `crev id new` to generate CrevID.")]
59    UserConfigNotInitialized,
60
61    /// Use `auto_create_or_open` or fix potentially messed up config directory
62    #[error("User config already exists")]
63    UserConfigAlreadyExists,
64
65    /// User config loading error
66    #[error("User config loading error '{}': {}", _0.0.display(), _0.1)]
67    UserConfigLoadError(Box<(PathBuf, std::io::Error)>),
68
69    /// You've sandboxed too hard? We need to run Cargo
70    #[error("No valid home directory path could be retrieved from the operating system")]
71    NoHomeDirectory,
72
73    /// This stores your private key
74    #[error("Id loading error '{}': {}", _0.0.display(), _0.1)]
75    IdLoadError(Box<(PathBuf, std::io::Error)>),
76
77    /// Create a new Id
78    #[error("Id file not found.")]
79    IDFileNotFound,
80
81    /// Crev repos must be public
82    #[error("Couldn't clone {}: {}", _0.0, _0.1)]
83    CouldNotCloneGitHttpsURL(Box<(String, String)>),
84
85    /// We don't support anonymous reviews
86    #[error("No ids given.")]
87    NoIdsGiven,
88
89    /// There's no password reset. If you don't remember it, start over!
90    #[error("Incorrect passphrase")]
91    IncorrectPassphrase,
92
93    /// crev has a concept of a default/current Id
94    #[error("Current Id not set")]
95    CurrentIDNotSet,
96
97    /// crev has a concept of a default/current Id
98    #[error("Id not specified and current id not set")]
99    IDNotSpecifiedAndCurrentIDNotSet,
100
101    /// crev uses git checkouts, and needs to know their URLs. Delete the repo and try again.
102    #[error("origin has no url at {}", _0.display())]
103    OriginHasNoURL(Box<Path>),
104
105    /// crev created a dummy Id for you, but you still need to configure it
106    #[error("current Id has been created without a git URL")]
107    GitUrlNotConfigured,
108
109    /// Error iterating local db
110    #[error("Error iterating local ProofStore at {}: {}", _0.0.display(), _0.1)]
111    ErrorIteratingLocalProofStore(Box<(PathBuf, String)>),
112
113    /// blake_hash mismatch
114    #[error("File {} not current. Review again use `crev add` to update.", _0.display())]
115    FileNotCurrent(Box<Path>),
116
117    /// Needs config.yaml
118    #[error("Package config not-initialized. Use `crev package init` to generate it.")]
119    PackageConfigNotInitialized,
120
121    /// Wrong path given to git
122    #[error("Can't stage path from outside of the staging root")]
123    PathNotInStageRootPath,
124
125    /// Git is cursed
126    #[error("Git entry without a path")]
127    GitEntryWithoutAPath,
128
129    /// Sorry about YAML syntax
130    #[error(transparent)]
131    YAML(#[from] serde_yaml::Error),
132
133    /// Used for staging temp file
134    #[error(transparent)]
135    CBOR(#[from] serde_cbor::Error),
136
137    /// See [`repo::PackageDirNotFound`]
138    #[error(transparent)]
139    PackageDirNotFound(#[from] repo::PackageDirNotFound),
140
141    /// See [`crev_common::CancelledError`]
142    #[error(transparent)]
143    Cancelled(#[from] crev_common::CancelledError),
144
145    /// See [`crev_data::Error`]
146    #[error(transparent)]
147    Data(#[from] crev_data::Error),
148
149    /// See [`argon2::Error`]
150    #[error("Passphrase: {}", _0)]
151    Passphrase(#[from] argon2::Error),
152
153    /// YAML ;(
154    #[error("Review activity parse error: {}", _0)]
155    ReviewActivity(#[source] Box<crev_common::YAMLIOError>),
156
157    /// YAML ;(
158    #[error("Error parsing user config: {}", _0)]
159    UserConfigParse(#[source] serde_yaml::Error),
160
161    /// See [`crev_recursive_digest::DigestError`]
162    #[error(transparent)]
163    Digest(#[from] crev_recursive_digest::DigestError),
164
165    /// Misc problems with git repos
166    #[error(transparent)]
167    Git(#[from] git2::Error),
168
169    /// Misc problems with file I/O
170    #[error("I/O: {}", _0)]
171    IO(#[from] std::io::Error),
172
173    /// crev open makes cargo projects that don't run the code
174    #[error("Error while copying crate sources: {}", _0)]
175    CrateSourceSanitizationError(std::io::Error),
176
177    /// Misc problems with file I/O
178    #[error("Error writing to {}: {}", _1.display(), _0)]
179    FileWrite(std::io::Error, PathBuf),
180
181    /// See [`IdError`]
182    #[error(transparent)]
183    Id(#[from] IdError),
184}
185
186/// [`crate::Error`]
187type Result<T, E = Error> = std::result::Result<T, E>;
188
189/// Trait representing a place that can keep proofs (all reviews and trust proofs)
190///
191/// See [`::crev_wot::ProofDb`] and [`crate::Local`].
192///
193/// Typically serialized and persisted.
194#[doc(hidden)]
195pub trait ProofStore {
196    fn insert(&self, proof: &crev_data::proof::Proof) -> Result<()>;
197    fn proofs_iter(&self) -> Result<Box<dyn Iterator<Item = crev_data::proof::Proof>>>;
198}
199
200/// Your relationship to the person
201#[derive(Copy, Clone, PartialEq, Eq)]
202pub enum TrustProofType {
203    /// Positive
204    Trust,
205    /// Neutral (undo Trust)
206    Untrust,
207    /// Very negative. This is an attacker. Block everything by them.
208    Distrust,
209}
210
211impl fmt::Display for TrustProofType {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            TrustProofType::Trust => f.write_str("trust"),
215            TrustProofType::Distrust => f.write_str("distrust"),
216            TrustProofType::Untrust => f.write_str("untrust"),
217        }
218    }
219}
220
221impl TrustProofType {
222    /// Is this person trusted at all? (regardless of level of trust)
223    #[must_use]
224    pub fn is_trust(self) -> bool {
225        if let TrustProofType::Trust = self {
226            return true;
227        }
228        false
229    }
230
231    /// Make review template. See [`crev_data::Review`]
232    #[must_use]
233    pub fn to_review(self) -> crev_data::Review {
234        use TrustProofType::{Distrust, Trust, Untrust};
235        match self {
236            Trust => crev_data::Review::new_positive(),
237            Distrust => crev_data::Review::new_negative(),
238            Untrust => crev_data::Review::new_none(),
239        }
240    }
241}
242
243/// Verification requirements for filtering out low quality reviews
244///
245/// See [`crev_wot::TrustDistanceParams`]
246#[derive(Clone, Debug)]
247pub struct VerificationRequirements {
248    /// How much the reviewer must be trusted
249    pub trust_level: crev_data::Level,
250    /// How much code understanding reviewer has reported
251    pub understanding: crev_data::Level,
252    /// How much thoroughness reviewer has reported
253    pub thoroughness: crev_data::Level,
254    /// How many different reviews are required
255    pub redundancy: u64,
256}
257
258impl Default for VerificationRequirements {
259    fn default() -> Self {
260        VerificationRequirements {
261            trust_level: Default::default(),
262            understanding: Default::default(),
263            thoroughness: Default::default(),
264            redundancy: 1,
265        }
266    }
267}
268
269/// Result of verification
270///
271/// Not named `Result` to avoid confusion with `Result` type.
272#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
273pub enum VerificationStatus {
274    /// That's bad!
275    Negative,
276    /// VerificationRequirements set too high
277    Insufficient,
278    /// Okay
279    Verified,
280    /// This is your package, trust yourself.
281    Local,
282}
283
284impl VerificationStatus {
285    /// Is it `VerificationStatus::Verified`?
286    #[must_use]
287    pub fn is_verified(self) -> bool {
288        self == VerificationStatus::Verified
289    }
290
291    /// Pick worse of both
292    #[must_use]
293    pub fn min(self, other: Self) -> Self {
294        if self < other {
295            self
296        } else if other < self {
297            other
298        } else {
299            self
300        }
301    }
302}
303
304impl fmt::Display for VerificationStatus {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        match self {
307            VerificationStatus::Local => f.pad("locl"),
308            VerificationStatus::Verified => f.pad("pass"),
309            VerificationStatus::Insufficient => f.pad("none"),
310            VerificationStatus::Negative => f.pad("warn"),
311        }
312    }
313}
314
315/// Find reviews matching `Digest` (exact data of the crate)
316/// and see if there are enough positive reviews for it.
317pub fn verify_package_digest(
318    digest: &Digest,
319    trust_set: &crev_wot::TrustSet,
320    requirements: &VerificationRequirements,
321    db: &crev_wot::ProofDB,
322) -> VerificationStatus {
323    let reviews: HashMap<Id, review::Package> = db
324        .get_package_reviews_by_digest(digest)
325        .filter(|review| {
326            match trust_set
327                .package_review_ignore_override
328                .get(&PkgVersionReviewId::from(review))
329            {
330                Some(reporters) => {
331                    reporters.max_level().unwrap_or(TrustLevel::None)
332                        <= trust_set.get_effective_trust_level(&review.common.from.id)
333                }
334                None => true,
335            }
336        })
337        .map(|review| (review.from().id.clone(), review))
338        .collect();
339    // Faster somehow maybe?
340    let reviews_by: HashSet<Id, _> = reviews.keys().cloned().collect();
341    let trusted_ids: HashSet<_> = trust_set.get_trusted_ids();
342    let matching_reviewers = trusted_ids.intersection(&reviews_by);
343    let mut trust_count = 0;
344    let mut negative_count = 0;
345    for matching_reviewer in matching_reviewers {
346        let review = &reviews[matching_reviewer].review_possibly_none();
347        if !review.is_none()
348            && Rating::Neutral <= review.rating
349            && requirements.thoroughness <= review.thoroughness
350            && requirements.understanding <= review.understanding
351        {
352            if TrustLevel::from(requirements.trust_level)
353                <= trust_set.get_effective_trust_level(matching_reviewer)
354            {
355                trust_count += 1;
356            }
357        } else if review.rating <= Rating::Negative {
358            negative_count += 1;
359        }
360    }
361
362    if negative_count > 0 {
363        VerificationStatus::Negative
364    } else if trust_count >= requirements.redundancy {
365        VerificationStatus::Verified
366    } else {
367        VerificationStatus::Insufficient
368    }
369}
370
371/// Warnings gathered during operation, errors downgraded to warnings.
372#[derive(Debug, thiserror::Error)]
373pub enum Warning {
374    #[error(transparent)]
375    Error(#[from] Error),
376
377    #[error("Repo checkout without origin URL at {}", _0.display())]
378    NoRepoUrlAtPath(PathBuf, #[source] Error),
379
380    #[error("URL for {0} is not known yet")]
381    IdUrlNotKnonw(Id),
382
383    #[error("Could not deduce `ssh` push url for {0}. Call:\ncargo crev repo git remote set-url --push origin <url>\nmanually after the id is generated.")]
384    GitPushUrl(String),
385
386    #[error("Failed to fetch {0} into {path}", path = _2.display())]
387    FetchError(String, #[source] Error, PathBuf),
388}
389
390impl Warning {
391    #[must_use]
392    pub fn auto_log() -> LogOnDrop {
393        LogOnDrop(Vec::new())
394    }
395
396    pub fn log_all(warnings: &[Warning]) {
397        warnings.iter().for_each(|w| w.log());
398    }
399
400    pub fn log(&self) {
401        warn!("{}", self);
402        let mut s = self.source();
403        while let Some(w) = s {
404            warn!("  - {}", w);
405            s = w.source();
406        }
407    }
408}
409
410pub struct LogOnDrop(pub Vec<Warning>);
411impl Drop for LogOnDrop {
412    fn drop(&mut self) {
413        Warning::log_all(&self.0);
414    }
415}
416
417impl std::ops::Deref for LogOnDrop {
418    type Target = Vec<Warning>;
419    fn deref(&self) -> &Vec<Warning> {
420        &self.0
421    }
422}
423impl std::ops::DerefMut for LogOnDrop {
424    fn deref_mut(&mut self) -> &mut Vec<Warning> {
425        &mut self.0
426    }
427}
428
429/// Scan through known reviews of the crate (source is `"https://crates.io"`)
430/// and report semver you can safely use according to `requirements`
431///
432/// See also `verify_package_digest`
433pub fn find_latest_trusted_version(
434    trust_set: &crev_wot::TrustSet,
435    source: RegistrySource<'_>,
436    name: &str,
437    requirements: &crate::VerificationRequirements,
438    db: &crev_wot::ProofDB,
439) -> Option<Version> {
440    db.get_pkg_reviews_for_name(source, name)
441        .filter(|review| {
442            verify_package_digest(
443                &Digest::from_bytes(&review.package.digest).unwrap(),
444                trust_set,
445                requirements,
446                db,
447            )
448            .is_verified()
449        })
450        .max_by(|a, b| a.package.id.version.cmp(&b.package.id.version))
451        .map(|review| review.package.id.version.clone())
452}
453
454/// Check whether code at this path has reviews, and the reviews meet the requirements.
455///
456/// See also `verify_package_digest`
457pub fn dir_or_git_repo_verify(
458    path: &Path,
459    ignore_list: &fnv::FnvHashSet<PathBuf>,
460    db: &crev_wot::ProofDB,
461    trusted_set: &crev_wot::TrustSet,
462    requirements: &VerificationRequirements,
463) -> Result<crate::VerificationStatus> {
464    let digest = if path.join(".git").exists() {
465        get_recursive_digest_for_git_dir(path, ignore_list)?
466    } else {
467        Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap()
468    };
469
470    Ok(verify_package_digest(
471        &digest,
472        trusted_set,
473        requirements,
474        db,
475    ))
476}
477
478/// Check whether code at this path has reviews, and the reviews meet the requirements
479///
480/// Same as `dir_or_git_repo_verify`, except it doesn't handle .git dirs
481pub fn dir_verify(
482    path: &Path,
483    ignore_list: &fnv::FnvHashSet<PathBuf>,
484    db: &crev_wot::ProofDB,
485    trusted_set: &crev_wot::TrustSet,
486    requirements: &VerificationRequirements,
487) -> Result<crate::VerificationStatus> {
488    let digest = Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap();
489    Ok(verify_package_digest(
490        &digest,
491        trusted_set,
492        requirements,
493        db,
494    ))
495}
496
497/// Scan dir and hash everything in it, to get a unique identifier of the package's source code
498pub fn get_dir_digest(path: &Path, ignore_list: &fnv::FnvHashSet<PathBuf>) -> Result<Digest> {
499    Ok(Digest::from_bytes(&util::get_recursive_digest_for_dir(path, ignore_list)?).unwrap())
500}
501
502/// See `get_dir_digest`
503pub fn get_recursive_digest_for_git_dir(
504    root_path: &Path,
505    ignore_list: &fnv::FnvHashSet<PathBuf>,
506) -> Result<Digest> {
507    let git_repo = git2::Repository::open(root_path)?;
508
509    let mut status_opts = git2::StatusOptions::new();
510    let mut paths = HashSet::default();
511
512    status_opts.include_unmodified(true);
513    status_opts.include_untracked(false);
514    for entry in git_repo.statuses(Some(&mut status_opts))?.iter() {
515        let entry_path = PathBuf::from(entry.path().ok_or(Error::GitEntryWithoutAPath)?);
516        if ignore_list.contains(&entry_path) {
517            continue;
518        };
519
520        paths.insert(entry_path);
521    }
522
523    Ok(util::get_recursive_digest_for_paths(root_path, paths)?)
524}
525
526/// See `get_dir_digest`
527pub fn get_recursive_digest_for_paths(
528    root_path: &Path,
529    paths: fnv::FnvHashSet<PathBuf>,
530) -> Result<crev_data::Digest> {
531    Ok(util::get_recursive_digest_for_paths(root_path, paths)?)
532}
533
534/// See `get_dir_digest`
535pub fn get_recursive_digest_for_dir(
536    root_path: &Path,
537    rel_path_ignore_list: &fnv::FnvHashSet<PathBuf>,
538) -> Result<Digest> {
539    Ok(Digest::from_bytes(&util::get_recursive_digest_for_dir(
540        root_path,
541        rel_path_ignore_list,
542    )?)
543    .unwrap())
544}
545
546#[cfg(test)]
547mod tests;