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_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}