1use crate::host_identity::{verify_peer_process, HostExpectation, HostIdentityError, HostProcess};
31use crate::peer::PeerIdentity;
32use std::path::{Path, PathBuf};
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum SigningIdentity {
38 MacTeamId(String),
43 WindowsCommonName(String),
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct PeerPolicy {
53 signers: Vec<SigningIdentity>,
54 executables: Vec<PathBuf>,
55}
56
57impl PeerPolicy {
58 pub fn signed_by(identity: SigningIdentity) -> Self {
60 Self {
61 signers: vec![identity],
62 executables: Vec::new(),
63 }
64 }
65
66 #[must_use]
69 pub fn or_signed_by(mut self, identity: SigningIdentity) -> Self {
70 self.signers.push(identity);
71 self
72 }
73
74 #[must_use]
77 pub fn only_executable(mut self, path: impl Into<PathBuf>) -> Self {
78 self.executables.push(path.into());
79 self
80 }
81
82 pub fn signers(&self) -> &[SigningIdentity] {
84 &self.signers
85 }
86
87 pub fn host_expectation(&self) -> HostExpectation {
93 expectation(self)
94 }
95}
96
97pub const LICENSE_SNAPSHOT_METHOD: &str = "license.snapshot";
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum SignatureError {
108 NotSigned(String),
111 Unverifiable(String),
113 Unsupported(&'static str),
115}
116
117impl std::fmt::Display for SignatureError {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 SignatureError::NotSigned(why) => {
121 write!(f, "the code signature is not acceptable ({why})")
122 }
123 SignatureError::Unverifiable(why) => {
124 write!(f, "the code signature could not be checked ({why})")
125 }
126 SignatureError::Unsupported(why) => {
127 write!(f, "code signatures are not checked here ({why})")
128 }
129 }
130 }
131}
132
133impl std::error::Error for SignatureError {}
134
135pub fn verify_peer(
142 peer: &PeerIdentity,
143 policy: &PeerPolicy,
144) -> Result<HostProcess, HostIdentityError> {
145 verify_peer_process(Some(peer), &expectation(policy))
146}
147
148pub(crate) fn expectation(policy: &PeerPolicy) -> HostExpectation {
152 let signers = policy.signers.clone();
153 let check = move |host: &HostProcess| check_signature(host, &signers);
154 let mut expect = HostExpectation::any_executable_verified(check.clone());
155 if !policy.executables.is_empty() {
156 expect = HostExpectation::any_of(policy.executables.clone()).with_verifier(check);
157 }
158 expect
159}
160
161fn check_signature(host: &HostProcess, signers: &[SigningIdentity]) -> Result<(), String> {
162 if signers.is_empty() {
163 return Err("the policy names no signing identity".to_string());
164 }
165 platform::check(host, signers).map_err(|e| e.to_string())
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct AuthenticodeSigner {
171 pub common_name: String,
173}
174
175pub fn verify_authenticode(
180 path: &Path,
181 expected_common_name: Option<&str>,
182) -> Result<AuthenticodeSigner, SignatureError> {
183 platform::authenticode(path, expected_common_name)
184}
185
186#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
189pub(crate) fn mac_requirement(team_id: &str) -> Option<String> {
190 let valid = team_id.len() == 10
191 && team_id
192 .bytes()
193 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit());
194 valid.then(|| format!("anchor apple generic and certificate leaf[subject.OU] = \"{team_id}\""))
195}
196
197#[cfg_attr(not(windows), allow(dead_code))]
199pub(crate) fn signer_matches(found: &str, expected: &str) -> bool {
200 !found.is_empty() && found == expected
201}
202
203#[cfg(target_os = "macos")]
204mod platform {
205 use super::{mac_requirement, AuthenticodeSigner, SignatureError, SigningIdentity};
206 use crate::host_identity::HostProcess;
207 use core_foundation::base::TCFType;
208 use core_foundation::data::CFData;
209 use security_framework::os::macos::code_signing::{
210 Flags, GuestAttributes, SecCode, SecRequirement,
211 };
212 use std::path::Path;
213
214 pub(super) fn authenticode(
215 _path: &Path,
216 _expected: Option<&str>,
217 ) -> Result<AuthenticodeSigner, SignatureError> {
218 Err(SignatureError::Unsupported(
219 "Authenticode is a Windows signature",
220 ))
221 }
222
223 pub(super) fn check(
224 host: &HostProcess,
225 signers: &[SigningIdentity],
226 ) -> Result<(), SignatureError> {
227 let Some(token) = host.audit_token else {
230 return Err(SignatureError::Unverifiable(
231 "the kernel gave no audit token for the peer".into(),
232 ));
233 };
234 let mut teams = signers.iter().filter_map(|s| match s {
235 SigningIdentity::MacTeamId(team) => Some(team.as_str()),
236 _ => None,
237 });
238 let Some(first) = teams.next() else {
239 return Err(SignatureError::Unsupported(
240 "the policy names no macOS team identifier",
241 ));
242 };
243 let mut last = verify_audit_token(&token, first);
244 for team in teams {
245 if last.is_ok() {
246 break;
247 }
248 last = verify_audit_token(&token, team);
249 }
250 last
251 }
252
253 fn verify_audit_token(token: &[u32; 8], team_id: &str) -> Result<(), SignatureError> {
256 let Some(text) = mac_requirement(team_id) else {
257 return Err(SignatureError::Unverifiable(
258 "the configured team identifier is malformed".into(),
259 ));
260 };
261 let requirement: SecRequirement = text.parse().map_err(|error| {
262 SignatureError::Unverifiable(format!("requirement did not compile: {error}"))
263 })?;
264 let mut bytes = [0u8; 32];
265 for (i, word) in token.iter().enumerate() {
266 bytes[i * 4..i * 4 + 4].copy_from_slice(&word.to_ne_bytes());
267 }
268 let data = CFData::from_buffer(&bytes);
269 let mut attributes = GuestAttributes::new();
270 attributes.set_audit_token(data.as_concrete_TypeRef());
271 let guest = SecCode::copy_guest_with_attribues(None, &attributes, Flags::NONE).map_err(
272 |error| {
273 SignatureError::Unverifiable(format!(
274 "peer code object not found (OSStatus {}): {error}",
275 error.code()
276 ))
277 },
278 )?;
279 guest
280 .check_validity(Flags::NONE, &requirement)
281 .map_err(|error| {
282 SignatureError::NotSigned(format!(
283 "code requirement not satisfied (OSStatus {}): {error}",
284 error.code()
285 ))
286 })
287 }
288}
289
290#[cfg(windows)]
291mod platform {
292 use super::{signer_matches, AuthenticodeSigner, SignatureError, SigningIdentity};
293 use crate::host_identity::HostProcess;
294 use std::ffi::{c_void, OsString};
295 use std::os::windows::ffi::{OsStrExt, OsStringExt};
296 use std::path::Path;
297 use std::ptr::null_mut;
298
299 use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
300 use windows_sys::Win32::Security::Cryptography::{
301 szOID_COMMON_NAME, CertGetNameStringW, CERT_NAME_ATTR_TYPE,
302 };
303 use windows_sys::Win32::Security::WinTrust::{
304 WTHelperGetProvCertFromChain, WTHelperGetProvSignerFromChain,
305 WTHelperProvDataFromStateData, WinVerifyTrust, WINTRUST_ACTION_GENERIC_VERIFY_V2,
306 WINTRUST_DATA, WINTRUST_DATA_0, WINTRUST_FILE_INFO, WTD_CACHE_ONLY_URL_RETRIEVAL,
307 WTD_CHOICE_FILE, WTD_REVOKE_NONE, WTD_STATEACTION_CLOSE, WTD_STATEACTION_VERIFY,
308 WTD_UI_NONE,
309 };
310
311 pub(super) fn check(
312 host: &HostProcess,
313 signers: &[SigningIdentity],
314 ) -> Result<(), SignatureError> {
315 let names: Vec<&str> = signers
316 .iter()
317 .filter_map(|s| match s {
318 SigningIdentity::WindowsCommonName(cn) => Some(cn.as_str()),
319 _ => None,
320 })
321 .collect();
322 if names.is_empty() {
323 return Err(SignatureError::Unsupported(
324 "the policy names no Windows signer",
325 ));
326 }
327 let signer = authenticode(&host.executable, None)?;
328 if names.iter().any(|n| signer_matches(&signer.common_name, n)) {
329 Ok(())
330 } else {
331 Err(SignatureError::NotSigned(format!(
332 "signed by another publisher ({})",
333 signer.common_name
334 )))
335 }
336 }
337
338 pub(super) fn authenticode(
339 path: &Path,
340 expected: Option<&str>,
341 ) -> Result<AuthenticodeSigner, SignatureError> {
342 let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
343 unsafe {
347 let mut file = WINTRUST_FILE_INFO {
348 cbStruct: std::mem::size_of::<WINTRUST_FILE_INFO>() as u32,
349 pcwszFilePath: wide.as_ptr(),
350 ..Default::default()
351 };
352 let mut data = WINTRUST_DATA {
353 cbStruct: std::mem::size_of::<WINTRUST_DATA>() as u32,
354 dwUIChoice: WTD_UI_NONE,
355 fdwRevocationChecks: WTD_REVOKE_NONE,
356 dwUnionChoice: WTD_CHOICE_FILE,
357 Anonymous: WINTRUST_DATA_0 { pFile: &mut file },
358 dwStateAction: WTD_STATEACTION_VERIFY,
359 dwProvFlags: WTD_CACHE_ONLY_URL_RETRIEVAL,
360 ..Default::default()
361 };
362 let mut action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
363 let status = WinVerifyTrust(
364 INVALID_HANDLE_VALUE,
365 &mut action,
366 (&mut data as *mut WINTRUST_DATA).cast::<c_void>(),
367 );
368 let outcome = if status != 0 {
369 Err(SignatureError::NotSigned(format!(
370 "WinVerifyTrust 0x{:08X}",
371 status as u32
372 )))
373 } else {
374 match signer_common_name(data.hWVTStateData) {
375 Some(name) => match expected {
376 Some(expected) if !signer_matches(&name, expected) => {
377 Err(SignatureError::NotSigned(format!(
378 "signed by another publisher ({name})"
379 )))
380 }
381 _ => Ok(AuthenticodeSigner { common_name: name }),
382 },
383 None => Err(SignatureError::Unverifiable(
384 "signer certificate unreadable".into(),
385 )),
386 }
387 };
388 data.dwStateAction = WTD_STATEACTION_CLOSE;
389 WinVerifyTrust(
390 INVALID_HANDLE_VALUE,
391 &mut action,
392 (&mut data as *mut WINTRUST_DATA).cast::<c_void>(),
393 );
394 outcome
395 }
396 }
397
398 unsafe fn signer_common_name(state: HANDLE) -> Option<String> {
405 let provider = WTHelperProvDataFromStateData(state);
406 if provider.is_null() {
407 return None;
408 }
409 let signer = WTHelperGetProvSignerFromChain(provider, 0, 0, 0);
410 if signer.is_null() {
411 return None;
412 }
413 let cert = WTHelperGetProvCertFromChain(signer, 0);
414 if cert.is_null() || (*cert).pCert.is_null() {
415 return None;
416 }
417 let context = (*cert).pCert;
418 let oid = szOID_COMMON_NAME as *const c_void;
419 let needed = CertGetNameStringW(context, CERT_NAME_ATTR_TYPE, 0, oid, null_mut(), 0);
420 if needed <= 1 {
421 return None;
422 }
423 let mut buf = vec![0u16; needed as usize];
424 let written = CertGetNameStringW(
425 context,
426 CERT_NAME_ATTR_TYPE,
427 0,
428 oid,
429 buf.as_mut_ptr(),
430 needed,
431 );
432 if written <= 1 {
433 return None;
434 }
435 Some(
436 OsString::from_wide(&buf[..written as usize - 1])
437 .to_string_lossy()
438 .into_owned(),
439 )
440 }
441}
442
443#[cfg(not(any(target_os = "macos", windows)))]
444mod platform {
445 use super::{AuthenticodeSigner, SignatureError, SigningIdentity};
446 use crate::host_identity::HostProcess;
447 use std::path::Path;
448
449 pub(super) fn authenticode(
450 _path: &Path,
451 _expected: Option<&str>,
452 ) -> Result<AuthenticodeSigner, SignatureError> {
453 Err(SignatureError::Unsupported(
454 "Authenticode is a Windows signature",
455 ))
456 }
457
458 pub(super) fn check(
460 _host: &HostProcess,
461 _signers: &[SigningIdentity],
462 ) -> Result<(), SignatureError> {
463 Err(SignatureError::Unsupported(
464 "no peer code-signature verification on this platform",
465 ))
466 }
467}