use crate::host_identity::{verify_peer_process, HostExpectation, HostIdentityError, HostProcess};
use crate::peer::PeerIdentity;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SigningIdentity {
MacTeamId(String),
WindowsCommonName(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PeerPolicy {
signers: Vec<SigningIdentity>,
executables: Vec<PathBuf>,
}
impl PeerPolicy {
pub fn signed_by(identity: SigningIdentity) -> Self {
Self {
signers: vec![identity],
executables: Vec::new(),
}
}
#[must_use]
pub fn or_signed_by(mut self, identity: SigningIdentity) -> Self {
self.signers.push(identity);
self
}
#[must_use]
pub fn only_executable(mut self, path: impl Into<PathBuf>) -> Self {
self.executables.push(path.into());
self
}
pub fn signers(&self) -> &[SigningIdentity] {
&self.signers
}
pub fn host_expectation(&self) -> HostExpectation {
expectation(self)
}
}
pub const LICENSE_SNAPSHOT_METHOD: &str = "license.snapshot";
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SignatureError {
NotSigned(String),
Unverifiable(String),
Unsupported(&'static str),
}
impl std::fmt::Display for SignatureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignatureError::NotSigned(why) => {
write!(f, "the code signature is not acceptable ({why})")
}
SignatureError::Unverifiable(why) => {
write!(f, "the code signature could not be checked ({why})")
}
SignatureError::Unsupported(why) => {
write!(f, "code signatures are not checked here ({why})")
}
}
}
}
impl std::error::Error for SignatureError {}
pub fn verify_peer(
peer: &PeerIdentity,
policy: &PeerPolicy,
) -> Result<HostProcess, HostIdentityError> {
verify_peer_process(Some(peer), &expectation(policy))
}
pub(crate) fn expectation(policy: &PeerPolicy) -> HostExpectation {
let signers = policy.signers.clone();
let check = move |host: &HostProcess| check_signature(host, &signers);
let mut expect = HostExpectation::any_executable_verified(check.clone());
if !policy.executables.is_empty() {
expect = HostExpectation::any_of(policy.executables.clone()).with_verifier(check);
}
expect
}
fn check_signature(host: &HostProcess, signers: &[SigningIdentity]) -> Result<(), String> {
if signers.is_empty() {
return Err("the policy names no signing identity".to_string());
}
platform::check(host, signers).map_err(|e| e.to_string())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthenticodeSigner {
pub common_name: String,
}
pub fn verify_authenticode(
path: &Path,
expected_common_name: Option<&str>,
) -> Result<AuthenticodeSigner, SignatureError> {
platform::authenticode(path, expected_common_name)
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub(crate) fn mac_requirement(team_id: &str) -> Option<String> {
let valid = team_id.len() == 10
&& team_id
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit());
valid.then(|| format!("anchor apple generic and certificate leaf[subject.OU] = \"{team_id}\""))
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn signer_matches(found: &str, expected: &str) -> bool {
!found.is_empty() && found == expected
}
#[cfg(target_os = "macos")]
mod platform {
use super::{mac_requirement, AuthenticodeSigner, SignatureError, SigningIdentity};
use crate::host_identity::HostProcess;
use core_foundation::base::TCFType;
use core_foundation::data::CFData;
use security_framework::os::macos::code_signing::{
Flags, GuestAttributes, SecCode, SecRequirement,
};
use std::path::Path;
pub(super) fn authenticode(
_path: &Path,
_expected: Option<&str>,
) -> Result<AuthenticodeSigner, SignatureError> {
Err(SignatureError::Unsupported(
"Authenticode is a Windows signature",
))
}
pub(super) fn check(
host: &HostProcess,
signers: &[SigningIdentity],
) -> Result<(), SignatureError> {
let Some(token) = host.audit_token else {
return Err(SignatureError::Unverifiable(
"the kernel gave no audit token for the peer".into(),
));
};
let mut teams = signers.iter().filter_map(|s| match s {
SigningIdentity::MacTeamId(team) => Some(team.as_str()),
_ => None,
});
let Some(first) = teams.next() else {
return Err(SignatureError::Unsupported(
"the policy names no macOS team identifier",
));
};
let mut last = verify_audit_token(&token, first);
for team in teams {
if last.is_ok() {
break;
}
last = verify_audit_token(&token, team);
}
last
}
fn verify_audit_token(token: &[u32; 8], team_id: &str) -> Result<(), SignatureError> {
let Some(text) = mac_requirement(team_id) else {
return Err(SignatureError::Unverifiable(
"the configured team identifier is malformed".into(),
));
};
let requirement: SecRequirement = text.parse().map_err(|error| {
SignatureError::Unverifiable(format!("requirement did not compile: {error}"))
})?;
let mut bytes = [0u8; 32];
for (i, word) in token.iter().enumerate() {
bytes[i * 4..i * 4 + 4].copy_from_slice(&word.to_ne_bytes());
}
let data = CFData::from_buffer(&bytes);
let mut attributes = GuestAttributes::new();
attributes.set_audit_token(data.as_concrete_TypeRef());
let guest = SecCode::copy_guest_with_attribues(None, &attributes, Flags::NONE).map_err(
|error| {
SignatureError::Unverifiable(format!(
"peer code object not found (OSStatus {}): {error}",
error.code()
))
},
)?;
guest
.check_validity(Flags::NONE, &requirement)
.map_err(|error| {
SignatureError::NotSigned(format!(
"code requirement not satisfied (OSStatus {}): {error}",
error.code()
))
})
}
}
#[cfg(windows)]
mod platform {
use super::{signer_matches, AuthenticodeSigner, SignatureError, SigningIdentity};
use crate::host_identity::HostProcess;
use std::ffi::{c_void, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::Path;
use std::ptr::null_mut;
use windows_sys::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Security::Cryptography::{
szOID_COMMON_NAME, CertGetNameStringW, CERT_NAME_ATTR_TYPE,
};
use windows_sys::Win32::Security::WinTrust::{
WTHelperGetProvCertFromChain, WTHelperGetProvSignerFromChain,
WTHelperProvDataFromStateData, WinVerifyTrust, WINTRUST_ACTION_GENERIC_VERIFY_V2,
WINTRUST_DATA, WINTRUST_DATA_0, WINTRUST_FILE_INFO, WTD_CACHE_ONLY_URL_RETRIEVAL,
WTD_CHOICE_FILE, WTD_REVOKE_NONE, WTD_STATEACTION_CLOSE, WTD_STATEACTION_VERIFY,
WTD_UI_NONE,
};
pub(super) fn check(
host: &HostProcess,
signers: &[SigningIdentity],
) -> Result<(), SignatureError> {
let names: Vec<&str> = signers
.iter()
.filter_map(|s| match s {
SigningIdentity::WindowsCommonName(cn) => Some(cn.as_str()),
_ => None,
})
.collect();
if names.is_empty() {
return Err(SignatureError::Unsupported(
"the policy names no Windows signer",
));
}
let signer = authenticode(&host.executable, None)?;
if names.iter().any(|n| signer_matches(&signer.common_name, n)) {
Ok(())
} else {
Err(SignatureError::NotSigned(format!(
"signed by another publisher ({})",
signer.common_name
)))
}
}
pub(super) fn authenticode(
path: &Path,
expected: Option<&str>,
) -> Result<AuthenticodeSigner, SignatureError> {
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
unsafe {
let mut file = WINTRUST_FILE_INFO {
cbStruct: std::mem::size_of::<WINTRUST_FILE_INFO>() as u32,
pcwszFilePath: wide.as_ptr(),
..Default::default()
};
let mut data = WINTRUST_DATA {
cbStruct: std::mem::size_of::<WINTRUST_DATA>() as u32,
dwUIChoice: WTD_UI_NONE,
fdwRevocationChecks: WTD_REVOKE_NONE,
dwUnionChoice: WTD_CHOICE_FILE,
Anonymous: WINTRUST_DATA_0 { pFile: &mut file },
dwStateAction: WTD_STATEACTION_VERIFY,
dwProvFlags: WTD_CACHE_ONLY_URL_RETRIEVAL,
..Default::default()
};
let mut action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
let status = WinVerifyTrust(
INVALID_HANDLE_VALUE,
&mut action,
(&mut data as *mut WINTRUST_DATA).cast::<c_void>(),
);
let outcome = if status != 0 {
Err(SignatureError::NotSigned(format!(
"WinVerifyTrust 0x{:08X}",
status as u32
)))
} else {
match signer_common_name(data.hWVTStateData) {
Some(name) => match expected {
Some(expected) if !signer_matches(&name, expected) => {
Err(SignatureError::NotSigned(format!(
"signed by another publisher ({name})"
)))
}
_ => Ok(AuthenticodeSigner { common_name: name }),
},
None => Err(SignatureError::Unverifiable(
"signer certificate unreadable".into(),
)),
}
};
data.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(
INVALID_HANDLE_VALUE,
&mut action,
(&mut data as *mut WINTRUST_DATA).cast::<c_void>(),
);
outcome
}
}
unsafe fn signer_common_name(state: HANDLE) -> Option<String> {
let provider = WTHelperProvDataFromStateData(state);
if provider.is_null() {
return None;
}
let signer = WTHelperGetProvSignerFromChain(provider, 0, 0, 0);
if signer.is_null() {
return None;
}
let cert = WTHelperGetProvCertFromChain(signer, 0);
if cert.is_null() || (*cert).pCert.is_null() {
return None;
}
let context = (*cert).pCert;
let oid = szOID_COMMON_NAME as *const c_void;
let needed = CertGetNameStringW(context, CERT_NAME_ATTR_TYPE, 0, oid, null_mut(), 0);
if needed <= 1 {
return None;
}
let mut buf = vec![0u16; needed as usize];
let written = CertGetNameStringW(
context,
CERT_NAME_ATTR_TYPE,
0,
oid,
buf.as_mut_ptr(),
needed,
);
if written <= 1 {
return None;
}
Some(
OsString::from_wide(&buf[..written as usize - 1])
.to_string_lossy()
.into_owned(),
)
}
}
#[cfg(not(any(target_os = "macos", windows)))]
mod platform {
use super::{AuthenticodeSigner, SignatureError, SigningIdentity};
use crate::host_identity::HostProcess;
use std::path::Path;
pub(super) fn authenticode(
_path: &Path,
_expected: Option<&str>,
) -> Result<AuthenticodeSigner, SignatureError> {
Err(SignatureError::Unsupported(
"Authenticode is a Windows signature",
))
}
pub(super) fn check(
_host: &HostProcess,
_signers: &[SigningIdentity],
) -> Result<(), SignatureError> {
Err(SignatureError::Unsupported(
"no peer code-signature verification on this platform",
))
}
}