pray_core/client_trust/
enforce.rs1use 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}