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_git_repository, is_remote_git_url,
7    trust_git_run,
8};
9use super::policy::{best_rule, load_policy, normalize_key, ClientTrustRule};
10
11pub fn gate_git_source(
12    home: &Path,
13    source_url: &str,
14    repository: &Path,
15    revision: Option<&str>,
16) -> PrayResult<()> {
17    if !is_remote_git_url(source_url) {
18        return Ok(());
19    }
20    if !is_git_repository(repository) {
21        return Ok(());
22    }
23    enforce_source_trust(home, source_url, repository, revision)?;
24    if load_policy(home)?.is_some() {
25        super::prompt::prompt_untrusted_source_consent(home, source_url, repository)?;
26    }
27    Ok(())
28}
29
30pub fn enforce_source_trust(
31    home: &Path,
32    source_url: &str,
33    repository: &Path,
34    revision: Option<&str>,
35) -> PrayResult<()> {
36    let Some(policy) = load_policy(home)? else {
37        return Ok(());
38    };
39    let rule = best_rule(&policy, source_url);
40    if !rule.allow {
41        return Err(PrayError::Integrity(format!(
42            "source is blocked by trust policy: {source_url}"
43        )));
44    }
45    if rule.require_signed_commit {
46        enforce_signed_commit(
47            home,
48            source_url,
49            repository,
50            revision,
51            &rule.allowed_signing_keys,
52        )?;
53    }
54    Ok(())
55}
56
57fn enforce_signed_commit(
58    home: &Path,
59    source_url: &str,
60    repository: &Path,
61    revision: Option<&str>,
62    allowed_signing_keys: &[String],
63) -> PrayResult<()> {
64    let target = commit_ref(revision)?;
65    trust_git_run(home, source_url, repository, &["verify-commit", target]).map_err(|error| {
66        PrayError::Integrity(format!(
67            "trust policy requires signed commit, but {target} failed signature verification in {}: {error}",
68            repository.display()
69        ))
70    })?;
71
72    if allowed_signing_keys.is_empty() {
73        return Ok(());
74    }
75
76    let key = commit_signing_key(home, source_url, repository, revision)
77        .map(|value| normalize_key(&value));
78    let fingerprint = commit_signing_fingerprint(home, source_url, repository, revision)
79        .map(|value| normalize_key(&value));
80    let allowed: Vec<String> = allowed_signing_keys
81        .iter()
82        .map(|value| normalize_key(value))
83        .collect();
84    let key_ok = key
85        .as_ref()
86        .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value));
87    let fingerprint_ok = fingerprint
88        .as_ref()
89        .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value));
90    if key_ok || fingerprint_ok {
91        return Ok(());
92    }
93
94    Err(PrayError::Integrity(format!(
95        "trust policy signer mismatch: {target} signing key/fingerprint is not in allowed_signing_keys"
96    )))
97}
98
99fn commit_ref(revision: Option<&str>) -> PrayResult<&str> {
100    let Some(revision) = revision else {
101        return Ok("HEAD");
102    };
103    if revision.is_empty() || revision.starts_with('-') {
104        return Err(PrayError::Integrity(format!(
105            "refusing trust verification for unsafe revision {revision:?}"
106        )));
107    }
108    Ok(revision)
109}
110
111pub fn signer_matches_allowed(
112    home: &Path,
113    source_url: &str,
114    rule: &ClientTrustRule,
115    repository: &Path,
116) -> bool {
117    if rule.allowed_signing_keys.is_empty() {
118        return false;
119    }
120    let key =
121        commit_signing_key(home, source_url, repository, None).map(|value| normalize_key(&value));
122    let fingerprint = commit_signing_fingerprint(home, source_url, repository, None)
123        .map(|value| normalize_key(&value));
124    let allowed: Vec<String> = rule
125        .allowed_signing_keys
126        .iter()
127        .map(|value| normalize_key(value))
128        .collect();
129    key.as_ref()
130        .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value))
131        || fingerprint
132            .as_ref()
133            .is_some_and(|value| allowed.iter().any(|allowed_key| allowed_key == value))
134}
135
136pub fn env_truthy(name: &str) -> bool {
137    std::env::var_os(name).is_some_and(|value| {
138        let text = value.to_string_lossy();
139        matches!(
140            text.trim().to_ascii_lowercase().as_str(),
141            "1" | "true" | "yes" | "on"
142        )
143    })
144}