use minisign_verify::{PublicKey, Signature};
use semver::Version;
use serde::{Deserialize, Serialize};
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Manifest {
pub version: Version,
pub created_at: String,
pub min_shell_version: Version,
pub archive: ArchiveInfo,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArchiveInfo {
pub url: String,
pub sha256: String,
pub size: u64,
}
pub fn verify_and_parse(
manifest_bytes: &[u8],
signature: &str,
trusted_pubkeys: &[String],
) -> Result<Manifest> {
if trusted_pubkeys.is_empty() {
return Err(Error::InvalidPublicKey);
}
let keys = trusted_pubkeys
.iter()
.map(|key| parse_public_key(key))
.collect::<Result<Vec<_>>>()?;
let signature = Signature::decode(signature)
.map_err(|e| Error::ManifestSignature(format!("undecodable .minisig data: {e}")))?;
let verified = keys
.iter()
.any(|key| key.verify(manifest_bytes, &signature, false).is_ok());
if !verified {
return Err(Error::ManifestSignature(
"no trusted public key verified the manifest".into(),
));
}
let mut manifest: Manifest = serde_json::from_slice(manifest_bytes)?;
validate(&mut manifest)?;
Ok(manifest)
}
pub(crate) fn validate_pubkeys(keys: &[String]) -> Result<()> {
keys.iter()
.try_for_each(|key| parse_public_key(key).map(drop))
}
fn parse_public_key(key: &str) -> Result<PublicKey> {
let key = key.trim();
let result = if key.lines().count() > 1 {
PublicKey::decode(key)
} else {
PublicKey::from_base64(key)
};
result.map_err(|_| Error::InvalidPublicKey)
}
fn validate(manifest: &mut Manifest) -> Result<()> {
let sha = &manifest.archive.sha256;
if sha.len() != 64 || !sha.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(Error::ManifestInvalid(format!(
"archive.sha256 {sha:?} is not 64 hex characters"
)));
}
manifest.archive.sha256 = manifest.archive.sha256.to_ascii_lowercase();
let size = manifest.archive.size;
if size == 0 || size > crate::extract::MAX_UNCOMPRESSED_BYTES {
return Err(Error::ManifestInvalid(format!(
"archive.size {size} is outside 1..={}",
crate::extract::MAX_UNCOMPRESSED_BYTES
)));
}
Ok(())
}
#[cfg(test)]
mod tests;