Skip to main content

pray_core/client_trust/
enforce.rs

1use std::path::Path;
2
3use crate::{PrayError, PrayResult};
4
5use super::git::{
6    commit_signing_fingerprint, commit_signing_key, is_remote_git_url, trust_git_run,
7};
8use super::policy::{best_rule, load_policy, normalize_key, ClientTrustRule};
9
10pub fn gate_git_source(home: &Path, source_url: &str, repository: &Path) -> PrayResult<()> {
11    if !is_remote_git_url(source_url) {
12        return Ok(());
13    }
14    if !repository.join(".git").is_dir() {
15        return Ok(());
16    }
17    enforce_source_trust(home, source_url, repository)?;
18    if load_policy(home)?.is_some() {
19        super::prompt::prompt_untrusted_source_consent(home, source_url, repository)?;
20    }
21    Ok(())
22}
23
24pub fn enforce_source_trust(home: &Path, source_url: &str, repository: &Path) -> PrayResult<()> {
25    let Some(policy) = load_policy(home)? else {
26        return Ok(());
27    };
28    let rule = best_rule(&policy, source_url);
29    if !rule.allow {
30        return Err(PrayError::Integrity(format!(
31            "source is blocked by trust policy: {source_url}"
32        )));
33    }
34    if rule.require_signed_commit {
35        enforce_signed_commit(home, source_url, repository, &rule.allowed_signing_keys)?;
36    }
37    Ok(())
38}
39
40fn enforce_signed_commit(
41    home: &Path,
42    source_url: &str,
43    repository: &Path,
44    allowed_signing_keys: &[String],
45) -> PrayResult<()> {
46    trust_git_run(home, source_url, repository, &["verify-commit", "HEAD"]).map_err(|error| {
47        PrayError::Integrity(format!(
48            "trust policy requires signed commit, but HEAD failed signature verification in {}: {error}",
49            repository.display()
50        ))
51    })?;
52
53    if allowed_signing_keys.is_empty() {
54        return Ok(());
55    }
56
57    let key = commit_signing_key(home, source_url, repository).map(|value| normalize_key(&value));
58    let fingerprint =
59        commit_signing_fingerprint(home, source_url, repository).map(|value| normalize_key(&value));
60    let allowed: Vec<String> = allowed_signing_keys
61        .iter()
62        .map(|value| normalize_key(value))
63        .collect();
64    let key_ok = key
65        .as_ref()
66        .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value));
67    let fingerprint_ok = fingerprint
68        .as_ref()
69        .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value));
70    if key_ok || fingerprint_ok {
71        return Ok(());
72    }
73
74    Err(PrayError::Integrity(
75        "trust policy signer mismatch: HEAD signing key/fingerprint is not in allowed_signing_keys"
76            .into(),
77    ))
78}
79
80pub fn signer_matches_allowed(
81    home: &Path,
82    source_url: &str,
83    rule: &ClientTrustRule,
84    repository: &Path,
85) -> bool {
86    if rule.allowed_signing_keys.is_empty() {
87        return false;
88    }
89    let key = commit_signing_key(home, source_url, repository).map(|value| normalize_key(&value));
90    let fingerprint =
91        commit_signing_fingerprint(home, source_url, repository).map(|value| normalize_key(&value));
92    let allowed: Vec<String> = rule
93        .allowed_signing_keys
94        .iter()
95        .map(|value| normalize_key(value))
96        .collect();
97    key.as_ref()
98        .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value))
99        || fingerprint
100            .as_ref()
101            .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value))
102}
103
104pub fn env_truthy(name: &str) -> bool {
105    std::env::var(name)
106        .map(|value| {
107            matches!(
108                value.to_ascii_lowercase().as_str(),
109                "1" | "true" | "yes" | "on"
110            )
111        })
112        .unwrap_or(false)
113}