use base64::Engine as _;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use crate::error::UpdateError;
pub fn ed25519(
asset_filename: &str,
sig_filename: &str,
asset_bytes: &[u8],
sig_bytes: &[u8],
trusted_keys: &[[u8; 32]],
) -> crate::error::Result<()> {
if trusted_keys.is_empty() {
return Err(UpdateError::NoPublicKey);
}
let sig_raw = if sig_filename.ends_with(".minisig") {
parse_minisign(sig_bytes)
.ok_or_else(|| UpdateError::BadSignature { asset: asset_filename.to_string() })?
} else if sig_bytes.len() == 64 {
let mut out = [0u8; 64];
out.copy_from_slice(sig_bytes);
out
} else {
return Err(UpdateError::BadSignature { asset: asset_filename.to_string() });
};
let sig = Signature::from_bytes(&sig_raw);
for key_bytes in trusted_keys {
let Ok(vk) = VerifyingKey::from_bytes(key_bytes) else {
continue;
};
if vk.verify(asset_bytes, &sig).is_ok() {
return Ok(());
}
}
Err(UpdateError::BadSignature { asset: asset_filename.to_string() })
}
fn parse_minisign(bytes: &[u8]) -> Option<[u8; 64]> {
let text = std::str::from_utf8(bytes).ok()?;
for line in text.lines() {
let l = line.trim();
if l.is_empty() || l.starts_with("untrusted") || l.starts_with("trusted") {
continue;
}
let decoded = base64::engine::general_purpose::STANDARD.decode(l).ok()?;
if decoded.len() != 74 {
return None;
}
if decoded[0..2] != *b"Ed" {
return None;
}
let mut sig = [0u8; 64];
sig.copy_from_slice(&decoded[10..74]);
return Some(sig);
}
None
}
#[must_use]
pub fn sha256_hex(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write as _;
let _ = write!(out, "{byte:02x}");
}
out
}
pub fn checksums(
asset_filename: &str,
asset_bytes: &[u8],
checksums_file: &str,
) -> crate::error::Result<()> {
let actual = sha256_hex(asset_bytes);
let needle = std::path::Path::new(asset_filename)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(asset_filename);
for line in checksums_file.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.splitn(2, char::is_whitespace);
let Some(hex) = parts.next() else { continue };
let Some(file) = parts.next() else { continue };
let file = file.trim_start().trim_start_matches('*').trim();
if file == needle {
return if hex.eq_ignore_ascii_case(&actual) {
Ok(())
} else {
Err(UpdateError::BadChecksum { asset: asset_filename.to_string() })
};
}
}
Err(UpdateError::BadChecksum { asset: asset_filename.to_string() })
}