use ciborium::value::Value;
use ed25519_dalek::VerifyingKey;
use crate::cose::verify_signatures;
use crate::emojihash::{emojihash, emojihash_labels, randomart};
use crate::model::{Diagnostic, Graph};
use crate::openpgp::parse_transport_key;
use crate::policy::{
evaluate_profile_policy, signature_trust, ProfileFinding, Severity, TrustPolicy,
};
use crate::reader::read;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EmbeddedTransportKey {
pub kid: String,
pub gpg: String,
}
#[derive(Clone, Debug)]
pub struct VerifyOptions {
pub armored_key: Option<String>,
pub require_signatures: bool,
pub trust_policy: TrustPolicy,
}
impl Default for VerifyOptions {
fn default() -> Self {
Self {
armored_key: None,
require_signatures: true,
trust_policy: TrustPolicy::default(),
}
}
}
impl VerifyOptions {
pub fn strict() -> Self {
Self::default()
}
pub fn with_armored_key(mut self, armored: impl Into<String>) -> Self {
self.armored_key = Some(armored.into());
self
}
pub fn require_signatures(mut self, value: bool) -> Self {
self.require_signatures = value;
self
}
pub fn trust_policy(mut self, policy: TrustPolicy) -> Self {
self.trust_policy = policy;
self
}
}
#[derive(Clone, Debug, Default)]
pub struct VerificationResult {
pub ok: bool,
pub kid: Option<String>,
pub fingerprint: Option<String>,
pub emojihash: Option<String>,
pub emojihash_labels: Option<String>,
pub randomart: Option<String>,
pub frames: usize,
pub signed: usize,
pub valid: usize,
pub trusted: usize,
pub invalid: usize,
pub unverified: usize,
pub errors: Vec<String>,
pub diagnostics: Vec<Diagnostic>,
pub profile_findings: Vec<ProfileFinding>,
}
pub fn format_fingerprint(fingerprint: &str) -> String {
let compact: String = fingerprint.chars().filter(|c| !c.is_whitespace()).collect();
let compact = compact.to_uppercase();
if compact.is_empty() || !compact.bytes().all(|b| b.is_ascii_hexdigit()) {
return fingerprint.to_string();
}
compact
.as_bytes()
.chunks(4)
.map(|c| std::str::from_utf8(c).expect("hex is ascii"))
.collect::<Vec<_>>()
.join(" ")
}
pub fn extract_transport_key(graph: &Graph) -> Option<EmbeddedTransportKey> {
let value = graph
.meta
.iter()
.find(|(k, _)| k == "gts:transportKey")
.map(|(_, v)| v)?;
let Value::Map(entries) = value else {
return None;
};
let mut kid = None;
let mut gpg = None;
for (key, value) in entries {
if let (Value::Text(key), Value::Text(text)) = (key, value) {
match key.as_str() {
"kid" => kid = Some(text.clone()),
"gpg" => gpg = Some(text.clone()),
_ => {}
}
}
}
Some(EmbeddedTransportKey {
kid: kid?,
gpg: gpg?,
})
}
pub fn verify_file(data: &[u8]) -> VerificationResult {
verify_file_with_options(data, &VerifyOptions::strict())
}
pub fn verify_file_with_options(data: &[u8], options: &VerifyOptions) -> VerificationResult {
let mut errors = Vec::new();
let (kid, public, raw_public, fingerprint, graph): (
String,
VerifyingKey,
[u8; 32],
String,
Option<Graph>,
) = if let Some(armored) = options.armored_key.as_deref() {
match provider_from_armor(armored, None) {
Ok((kid, public, raw_public, fingerprint)) => {
(kid, public, raw_public, fingerprint, None)
}
Err(err) => {
errors.push(format!("cannot load trusted key: {err}"));
return VerificationResult {
ok: false,
errors,
..VerificationResult::default()
};
}
}
} else {
let first = read(data, true, None);
let Some(transport) = extract_transport_key(&first) else {
if !options.require_signatures && first.signatures.is_empty() {
return VerificationResult {
ok: true,
frames: first.signatures.len(),
diagnostics: first.diagnostics,
..VerificationResult::default()
};
}
errors.push("no gts:transportKey found in file metadata".to_string());
return VerificationResult {
ok: false,
errors,
diagnostics: first.diagnostics,
frames: first.signatures.len(),
signed: first.signatures.len(),
..VerificationResult::default()
};
};
match provider_from_armor(&transport.gpg, Some(&transport.kid)) {
Ok((kid, public, raw_public, fingerprint)) => {
(kid, public, raw_public, fingerprint, Some(first))
}
Err(err) => {
errors.push(format!("cannot load embedded transport key: {err}"));
return VerificationResult {
ok: false,
kid: Some(transport.kid),
errors,
diagnostics: first.diagnostics,
frames: first.signatures.len(),
signed: first.signatures.len(),
..VerificationResult::default()
};
}
}
};
let mut graph = graph.unwrap_or_else(|| read(data, true, None));
verify_signatures(&mut graph.signatures, |candidate| {
(candidate == kid).then_some(public)
});
let signed = graph.signatures.len();
let valid = graph
.signatures
.iter()
.filter(|sig| sig.status == "valid")
.count();
let invalid = graph
.signatures
.iter()
.filter(|sig| sig.status == "invalid")
.count();
let unverified = graph
.signatures
.iter()
.filter(|sig| sig.status == "unverified")
.count();
let trusts = signature_trust(&graph, Some(&options.trust_policy));
let trusted = trusts.iter().filter(|item| item.trusted).count();
let profile_findings = evaluate_profile_policy(&graph, Some(&options.trust_policy), None);
if invalid > 0 {
errors.push(format!("{invalid} signature(s) invalid"));
}
if unverified > 0 {
errors.push(format!(
"{unverified} signature(s) unverified (no key resolved)"
));
}
if options.require_signatures && signed == 0 {
errors.push("no signed frames found".to_string());
}
let has_profile_error = profile_findings
.iter()
.any(|finding| finding.severity == Severity::Error);
let ok = errors.is_empty() && invalid == 0 && unverified == 0 && !has_profile_error;
VerificationResult {
ok,
kid: Some(kid),
fingerprint: Some(fingerprint),
emojihash: Some(emojihash(&raw_public, 11)),
emojihash_labels: Some(emojihash_labels(&raw_public, 11)),
randomart: Some(randomart(&raw_public, "GTS transport")),
frames: signed,
signed,
valid,
trusted,
invalid,
unverified,
errors,
diagnostics: graph.diagnostics,
profile_findings,
}
}
fn provider_from_armor(
armored: &str,
kid: Option<&str>,
) -> Result<(String, VerifyingKey, [u8; 32], String), String> {
let parsed = parse_transport_key(armored).map_err(|e| e.to_string())?;
let public = VerifyingKey::from_bytes(&parsed.raw_public).map_err(|e| e.to_string())?;
let resolved_kid = kid.unwrap_or(&parsed.fingerprint).to_string();
Ok((resolved_kid, public, parsed.raw_public, parsed.fingerprint))
}