diff --git a/crates/ely_app/src/services/plugin_packages.rs b/crates/ely_app/src/services/plugin_packages.rs index 43003e2..1c5fdb5 100644 --- a/crates/ely_app/src/services/plugin_packages.rs +++ b/crates/ely_app/src/services/plugin_packages.rs @@ -312,6 +312,9 @@ mod tests { use super::PluginPackageReader; use crate::services::plugin_package_fixtures::{sign_package_in_place, write_signed_package}; + use crate::services::plugin_signatures::{ + PluginSignatureVerificationError, PluginSignatureVerifier, TrustedPluginSigningKeys, + }; #[test] fn reads_verified_directory_package() -> Result<(), Box> { @@ -325,6 +328,48 @@ mod tests { Ok(()) } + #[test] + fn reads_package_with_trusted_signing_key() -> Result<(), Box> { + let package = write_package("trusted", b"wasm component")?; + let manifest = package_manifest(package.as_path())?; + let trusted_keys = TrustedPluginSigningKeys::from_entries([( + manifest.signature().key_id().to_string(), + manifest.signature().public_key().to_string(), + )])?; + + PluginSignatureVerifier::verify_with_trusted_keys( + package.as_path(), + &manifest, + &trusted_keys, + )?; + + assert_eq!(manifest.id().as_str(), "com.elydora.trusted"); + Ok(()) + } + + #[test] + fn rejects_package_with_untrusted_signing_key() -> Result<(), Box> { + let package = write_package("untrusted", b"wasm component")?; + let trusted_keys = TrustedPluginSigningKeys::from_entries(Vec::<(&str, &str)>::new())?; + + let manifest = package_manifest(package.as_path())?; + + let error = PluginSignatureVerifier::verify_with_trusted_keys( + package.as_path(), + &manifest, + &trusted_keys, + ) + .err() + .ok_or_else(|| std::io::Error::other("trusted package verification succeeded"))?; + + assert!(matches!( + error, + PluginSignatureVerificationError::UntrustedPublicKey { key_id } + if key_id == "elydora-alpha-plugins" + )); + Ok(()) + } + #[test] fn rejects_package_checksum_mismatch() -> Result<(), Box> { let package = write_package("checksum", b"wasm component")?; @@ -396,6 +441,13 @@ mod tests { write_signed_package(temp_root()?.as_path(), name, component) } + fn package_manifest( + path: &std::path::Path, + ) -> Result> { + let manifest = fs::read_to_string(path.join("plugin.toml"))?; + Ok(ely_domain::PluginManifest::from_toml(manifest.as_str())?) + } + fn temp_root() -> Result> { let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); let path = std::env::temp_dir().join(format!("ely-plugin-package-{nanos}")); diff --git a/crates/ely_app/src/services/plugin_signatures.rs b/crates/ely_app/src/services/plugin_signatures.rs index f793b28..1008984 100644 --- a/crates/ely_app/src/services/plugin_signatures.rs +++ b/crates/ely_app/src/services/plugin_signatures.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeMap, ffi::OsStr, fs, io::{self, Read}, @@ -21,6 +22,18 @@ pub enum PluginSignatureVerificationError { #[error("plugin signature verification failed for key {key_id}")] VerificationFailed { key_id: String }, + #[error("trusted plugin signing key id is invalid: {value}")] + InvalidTrustedKeyId { value: String }, + + #[error("trusted plugin signing public key is invalid: {value}")] + InvalidTrustedPublicKey { value: String }, + + #[error("trusted plugin signing key is duplicated: {key_id}")] + DuplicateTrustedKey { key_id: String }, + + #[error("plugin signing key is not trusted: {key_id}")] + UntrustedPublicKey { key_id: String }, + #[error("failed to read plugin package entry {entry}: {path}")] ReadFailed { entry: &'static str, path: PathBuf, source: io::Error }, @@ -34,13 +47,66 @@ pub enum PluginSignatureVerificationError { UnsupportedEntry { path: PathBuf }, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TrustedPluginSigningKeys { + keys: BTreeMap, +} + pub struct PluginSignatureVerifier; +impl TrustedPluginSigningKeys { + pub fn from_entries(entries: I) -> Result + where + I: IntoIterator, + K: Into, + P: Into, + { + let mut keys = BTreeMap::new(); + for (key_id, public_key) in entries { + let key_id = trusted_key_id(key_id.into())?; + let public_key = trusted_public_key(public_key.into())?; + if keys.insert(key_id.clone(), public_key).is_some() { + return Err(PluginSignatureVerificationError::DuplicateTrustedKey { key_id }); + } + } + + Ok(Self { keys }) + } + + pub fn require_manifest( + &self, + manifest: &PluginManifest, + ) -> Result<(), PluginSignatureVerificationError> { + let signature = manifest.signature(); + match self.keys.get(signature.key_id()) { + Some(public_key) if public_key == signature.public_key() => Ok(()), + _ => Err(PluginSignatureVerificationError::UntrustedPublicKey { + key_id: signature.key_id().to_string(), + }), + } + } + + fn from_manifest(manifest: &PluginManifest) -> Result { + let signature = manifest.signature(); + Self::from_entries([(signature.key_id().to_string(), signature.public_key().to_string())]) + } +} + impl PluginSignatureVerifier { pub fn verify( package_root: &Path, manifest: &PluginManifest, ) -> Result<(), PluginSignatureVerificationError> { + let trusted_keys = TrustedPluginSigningKeys::from_manifest(manifest)?; + Self::verify_with_trusted_keys(package_root, manifest, &trusted_keys) + } + + pub fn verify_with_trusted_keys( + package_root: &Path, + manifest: &PluginManifest, + trusted_keys: &TrustedPluginSigningKeys, + ) -> Result<(), PluginSignatureVerificationError> { + trusted_keys.require_manifest(manifest)?; let key_id = manifest.signature().key_id().to_string(); let public_key = decode_hex_array::<32>(manifest.signature().public_key()).ok_or_else(|| { @@ -90,6 +156,26 @@ pub(crate) fn signing_payload( Ok(payload) } +fn trusted_key_id(value: String) -> Result { + let value = value.trim().to_string(); + let valid = (3..=128).contains(&value.len()) + && value.chars().all(|ch| { + ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '-' | '_') + }); + if !valid { + return Err(PluginSignatureVerificationError::InvalidTrustedKeyId { value }); + } + Ok(value) +} + +fn trusted_public_key(value: String) -> Result { + let value = value.trim().to_ascii_lowercase(); + if decode_hex_array::<32>(value.as_str()).is_none() { + return Err(PluginSignatureVerificationError::InvalidTrustedPublicKey { value }); + } + Ok(value) +} + fn package_files_hash(package_root: &Path) -> Result { let mut entries = Vec::new(); collect_signed_entries(package_root, Path::new(""), &mut entries)?;