use std::{
collections::{BTreeMap, HashSet},
fmt::Debug,
io::stdin,
path::{Path, PathBuf},
};
use log::info;
use voa_config::{TechnologySettings, VoaConfig};
use voa_core::{
LoadPathList,
Verifier,
VerifierWriter,
Voa,
identifiers::{Context, Os, Purpose, Technology},
};
use voa_openpgp::{
ModelBasedVerifier,
OpenPgpImport,
OpenpgpCert,
OpenpgpSignature,
OpenpgpSignatureCheck,
VoaOpenpgp,
import::destructured::load_from_dir,
};
use crate::{
Error,
utils::{DirOrFile, DirOrFileType, RegularFile},
};
pub fn get_writable_load_path(runtime: bool) -> Result<PathBuf, Error> {
let load_path_list = LoadPathList::from_effective_user();
let filter = voa_core::LoadPathFilter {
ephemeral: runtime,
writable: true,
};
let load_path = load_path_list
.filter(&filter)
.first()
.cloned()
.ok_or(Error::NoLoadPath)?;
Ok(load_path.path.clone())
}
pub fn load_verifier(
input: Option<DirOrFile>,
technology: Technology,
) -> Result<impl VerifierWriter + Debug, Error> {
match technology {
Technology::Openpgp => Ok(if let Some(path) = input {
match path.typ {
DirOrFileType::Dir => load_from_dir(&path)?,
DirOrFileType::File => OpenPgpImport::from_file(&path)?,
}
} else {
OpenPgpImport::from_reader(stdin())?
}),
technology => Err(Error::UnsupportedTechnology { technology }),
}
}
pub fn write_verifier_to_hierarchy(
verifier: impl VerifierWriter,
base_path: impl AsRef<Path>,
os: Os,
purpose: Purpose,
context: Option<Context>,
) -> Result<(), Error> {
let base_path = base_path.as_ref();
info!("Writing verifier to VOA base path: {base_path:?}");
verifier.write_to_hierarchy(base_path, os, purpose, context)?;
Ok(())
}
pub fn search_verifiers(
os: Os,
purpose: Purpose,
context: Option<Context>,
technology: Option<Technology>,
) -> Result<BTreeMap<PathBuf, Vec<Verifier>>, Error> {
let context = if let Some(context) = context {
context
} else {
Context::Default
};
let technology = if let Some(technology) = technology {
technology
} else {
Technology::Openpgp
};
let voa = Voa::new();
let verifiers = voa.lookup(os, purpose, context, technology);
Ok(verifiers)
}
pub fn read_openpgp_signatures(
signatures: &HashSet<RegularFile>,
) -> Result<Vec<OpenpgpSignature>, Error> {
let mut openpgp_sigs = Vec::new();
for path in signatures {
info!("Reading {:?} as OpenPGP signature", path.as_ref());
openpgp_sigs.push(OpenpgpSignature::from_file(path).map_err(Error::VoaOpenPgp)?)
}
Ok(openpgp_sigs)
}
pub fn read_openpgp_verifiers(os: Os, purpose: Purpose, context: Context) -> Vec<OpenpgpCert> {
info!(
"Reading all OpenPGP certificates matching os={os}, purpose={purpose}, context={context}"
);
let voa = VoaOpenpgp::new();
voa.lookup(os, purpose, context)
}
pub fn openpgp_verify<'a>(
model_verifier: &'a ModelBasedVerifier,
signatures: &'a [OpenpgpSignature],
file: &RegularFile,
) -> Result<Vec<OpenpgpSignatureCheck<'a>>, Error> {
model_verifier
.verify_file_with_signatures(file, signatures)
.map_err(Error::VoaOpenPgp)
}
pub fn get_voa_config() -> VoaConfig {
VoaConfig::load()
}
#[derive(Debug)]
pub struct PurposeAndContext {
purpose: Purpose,
context: Context,
}
impl PurposeAndContext {
pub fn new(purpose: Option<Purpose>, context: Option<Context>) -> Option<Self> {
let purpose = purpose?;
let context = context.unwrap_or_default();
Some(Self { purpose, context })
}
pub fn purpose(&self) -> &Purpose {
&self.purpose
}
pub fn context(&self) -> &Context {
&self.context
}
}
pub fn get_technology_settings<'a>(
config: &'a VoaConfig,
os: &Os,
purpose_and_context: Option<&PurposeAndContext>,
) -> &'a TechnologySettings {
if let Some(purpose_and_context) = purpose_and_context {
config.settings_for_context_or_default(
os,
purpose_and_context.purpose(),
purpose_and_context.context(),
)
} else {
config.settings_for_os_or_default(os)
}
}
#[cfg(test)]
mod tests {
use libc::geteuid;
use rstest::rstest;
use testresult::TestResult;
use super::*;
#[rstest]
#[case::runtime_dir(true)]
#[case::config_dir(false)]
fn get_writable_load_path_succeeds(#[case] runtime: bool) -> TestResult {
let load_path = get_writable_load_path(runtime)?;
let euid = unsafe { geteuid() };
eprintln!("Load path: {load_path:?}");
if runtime {
assert!(load_path.starts_with("/run"))
} else if euid < 1000 {
assert_eq!(load_path, PathBuf::from("/etc/voa"))
} else {
assert!(load_path.ends_with(".config/voa"))
}
Ok(())
}
#[test]
fn load_verifier_fails_on_unsupported_technology() -> TestResult {
let result = load_verifier(None, Technology::Custom("foo".parse()?));
match result {
Err(Error::UnsupportedTechnology { .. }) => {}
Err(error) => panic!("Did not raise Error::UnsupportedTechnology but {error}"),
Ok(verifier) => {
panic!("Is expected to fail, but succeeded to load verifier: {verifier:?}")
}
}
Ok(())
}
#[rstest]
#[case::purpose_and_context_is_none(
"example".parse()?,
PurposeAndContext::new(None, None),
)]
#[case::purpose_and_context_purpose_is_none(
"example".parse()?,
PurposeAndContext::new(None, Some("context".parse()?)),
)]
#[case::purpose_and_context_context_is_none(
"example".parse()?,
PurposeAndContext::new(Some("purpose".parse()?), None),
)]
#[case::purpose_and_context_is_some(
"example".parse()?,
PurposeAndContext::new(Some("purpose".parse()?), Some("context".parse()?)),
)]
fn get_technology_settings_os_or_context(
#[case] os: Os,
#[case] purpose_and_context: Option<PurposeAndContext>,
) -> TestResult {
let config = get_voa_config();
let settings = get_technology_settings(&config, &os, purpose_and_context.as_ref());
println!("{settings}");
Ok(())
}
}