use std::{
collections::{BTreeMap, HashSet},
fmt::Display,
path::PathBuf,
process::ExitCode,
time::UNIX_EPOCH,
};
use clap::Parser;
use log::LevelFilter;
use serde::Serialize;
use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
use voa::{
Error,
cli::{Cli, Command, ConfigCommand, OutputFormat},
commands::{
PurposeAndContext,
get_technology_settings,
get_voa_config,
get_writable_load_path,
load_verifier,
openpgp_verify,
read_openpgp_signatures,
read_openpgp_verifiers,
search_verifiers,
write_verifier_to_hierarchy,
},
};
use voa_config::{ConfigOrigin, VoaConfig};
use voa_core::identifiers::{Context, Os, Purpose, Technology};
use voa_openpgp::{ModelBasedVerifier, OpenpgpSignatureCheck};
#[derive(Debug, Serialize)]
struct LoadOutput {
pub load_path: PathBuf,
pub writable: bool,
pub ephemeral: bool,
}
#[derive(Debug, Serialize)]
struct VerifierOutput {
pub load_path: LoadOutput,
pub verifier_path: PathBuf,
pub os: Os,
pub purpose: Purpose,
pub context: Context,
pub technology: Technology,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OpenpgpVerificationResult {
Valid {
signature_creation_time: Option<u64>,
primary_key_fingerprint: String,
verifying_component_key_fingerprint: String,
},
Invalid,
}
#[derive(Debug, Serialize)]
pub struct OpenpgpVerificationOutput {
pub signature: Option<PathBuf>,
pub result: OpenpgpVerificationResult,
}
impl Display for OpenpgpVerificationOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} ",
match &self.result {
OpenpgpVerificationResult::Valid { .. } => "✅",
OpenpgpVerificationResult::Invalid => "❌",
}
)?;
if let Some(signature_path) = self.signature.as_ref() {
write!(f, "{} ", signature_path.as_os_str().to_string_lossy())?;
} else {
write!(f, "- ")?;
}
match &self.result {
OpenpgpVerificationResult::Valid {
signature_creation_time,
primary_key_fingerprint,
verifying_component_key_fingerprint,
} => {
if let Some(signature_creation_time) = signature_creation_time {
write!(f, "{signature_creation_time} ")?;
} else {
write!(f, "- ")?;
}
write!(
f,
"{primary_key_fingerprint} {verifying_component_key_fingerprint}"
)?;
}
OpenpgpVerificationResult::Invalid => {
write!(f, "- - -")?;
}
}
Ok(())
}
}
impl<'a> From<&OpenpgpSignatureCheck<'a>> for OpenpgpVerificationOutput {
fn from(value: &OpenpgpSignatureCheck<'a>) -> Self {
let result = if let Some(signer_info) = value.signer_info() {
OpenpgpVerificationResult::Valid {
signature_creation_time: value.signature().creation_time().map(|time| {
time.duration_since(UNIX_EPOCH)
.expect("Time can't be before Unix epoch")
.as_secs()
}),
primary_key_fingerprint: signer_info
.certificate()
.certificate
.fingerprint()
.to_string(),
verifying_component_key_fingerprint: signer_info
.component_fingerprint()
.to_string(),
}
} else {
OpenpgpVerificationResult::Invalid
};
Self {
signature: value.signature().source().map(|path| path.to_path_buf()),
result,
}
}
}
#[derive(Debug, Serialize)]
struct ConfigOriginsOutput<'a> {
os: BTreeMap<String, &'a [ConfigOrigin]>,
os_purpose_context: BTreeMap<String, &'a [ConfigOrigin]>,
}
impl<'a> Display for ConfigOriginsOutput<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (os, origins) in &self.os {
writeln!(f, "🖥️ {os}")?;
for origin in origins.iter() {
writeln!(f, "⤷ {origin}")?;
}
}
for (os_purpose_context, origins) in &self.os_purpose_context {
writeln!(f, "➕️ {os_purpose_context}")?;
for origin in origins.iter() {
writeln!(f, "⤷ {origin}")?;
}
}
Ok(())
}
}
impl<'a> From<&'a VoaConfig> for ConfigOriginsOutput<'a> {
fn from(value: &'a VoaConfig) -> Self {
let os = value
.oses()
.iter()
.map(|(os, settings)| (os.to_string(), settings.origins()))
.collect::<BTreeMap<String, &[ConfigOrigin]>>();
let os_purpose_context = value
.contexts()
.iter()
.map(|(os_purpose_context, settings)| {
(os_purpose_context.to_string(), settings.origins())
})
.collect::<BTreeMap<String, &[ConfigOrigin]>>();
Self {
os,
os_purpose_context,
}
}
}
fn init_logger(log_level: LevelFilter) {
if TermLogger::init(
log_level,
Config::default(),
TerminalMode::Stderr,
ColorChoice::Auto,
)
.is_err()
{
eprintln!("Failed initializing a logger. Log messages may not be available!");
}
}
fn run_voa() -> Result<(), Error> {
let cli = Cli::parse();
init_logger(cli.verbosity.into());
match cli.command {
Command::Config(command) => {
let config = get_voa_config();
match command {
ConfigCommand::List(command) => {
let config_paths = ConfigOriginsOutput::from(&config);
match command.output_format {
OutputFormat::Text => {
println!("{config_paths}");
}
OutputFormat::Json => {
let output = serde_json::to_string(&config_paths).map_err(
|source| Error::JsonSerialization {
context: "processing the output of the voa config list subcommand",
source,
},
)?;
println!("{output}");
}
}
}
ConfigCommand::Show(command) => {
let technology_settings = get_technology_settings(
&config,
&command.os,
PurposeAndContext::new(command.purpose, command.context).as_ref(),
);
match command.output_format {
OutputFormat::Text => {
println!("{technology_settings}");
}
OutputFormat::Json => {
let output = serde_json::to_string(&technology_settings).map_err(
|source| Error::JsonSerialization {
context: "processing the output of the voa config show subcommand",
source,
},
)?;
println!("{output}");
}
}
}
}
}
Command::Import(command) => {
let base_path = if let Some(base_path) = command.base_path {
base_path
} else {
get_writable_load_path(command.runtime)?
};
let verifier = load_verifier(command.input, command.technology)?;
write_verifier_to_hierarchy(
verifier,
base_path,
command.os,
command.purpose,
command.context,
)?;
}
Command::List(command) => {
let verifiers = search_verifiers(
command.os,
command.purpose,
command.context,
command.technology,
)?;
let verifiers: Vec<VerifierOutput> = verifiers
.values()
.flat_map(|vec| vec.iter())
.map(|verifier| VerifierOutput {
load_path: LoadOutput {
load_path: verifier.voa_location().load_path().path().into(),
writable: verifier.voa_location().load_path().writable(),
ephemeral: verifier.voa_location().load_path().ephemeral(),
},
verifier_path: verifier.canonicalized().into(),
os: verifier.voa_location().os().clone(),
purpose: verifier.voa_location().purpose().clone(),
context: verifier.voa_location().context().clone(),
technology: verifier.voa_location().technology().clone(),
})
.collect();
match command.output_format {
OutputFormat::Text => {
for verifier in verifiers {
println!("{}", verifier.verifier_path.as_os_str().to_string_lossy())
}
}
OutputFormat::Json => {
let output = serde_json::to_string(&verifiers).map_err(|source| {
Error::JsonSerialization {
context: "processing the output of the voa list subcommand",
source,
}
})?;
println!("{output}");
}
}
}
Command::Verify(command) => {
let config = get_voa_config();
let purpose = command.purpose.as_ref();
let purpose_and_context =
PurposeAndContext::new(Some(purpose.clone()), Some(command.context.clone()));
let openpgp_settings =
get_technology_settings(&config, &command.os, purpose_and_context.as_ref())
.openpgp_settings();
let verifiers = read_openpgp_verifiers(
command.os.clone(),
purpose.clone(),
command.context.clone(),
);
let anchors = read_openpgp_verifiers(
command.os,
purpose.clone().to_trust_anchor(),
command.context,
);
let model = ModelBasedVerifier::new(openpgp_settings, &verifiers, &anchors);
let signatures = read_openpgp_signatures(&HashSet::from_iter(command.signatures))?;
let verifications = openpgp_verify(&model, &signatures, &command.file)?;
let verification_output: Vec<OpenpgpVerificationOutput> = verifications
.iter()
.map(OpenpgpVerificationOutput::from)
.collect();
match command.output_format {
OutputFormat::Text => {
for verification in &verification_output {
println!("{verification}");
}
}
OutputFormat::Json => {
let output = serde_json::to_string(&verification_output).map_err(|source| {
Error::JsonSerialization {
context: "processing the output of the voa verify subcommand",
source,
}
})?;
println!("{output}");
}
}
let failed_signatures: Vec<String> = verification_output
.iter()
.filter_map(|verification| {
if matches!(verification.result, OpenpgpVerificationResult::Invalid) {
if let Some(path) = verification.signature.as_ref() {
Some(path.to_string_lossy().to_string())
} else {
Some("-".to_string())
}
} else {
None
}
})
.collect();
if !failed_signatures.is_empty() {
return Err(Error::SignatureVerificationFailed {
signatures: failed_signatures,
});
}
}
}
Ok(())
}
fn main() -> ExitCode {
if let Err(error) = run_voa() {
eprintln!("{error}");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}