Support trusted plugin signing keys

This commit is contained in:
2026-05-07 23:52:25 -04:00
parent aca38c20f8
commit 90c5110d6c
2 changed files with 138 additions and 0 deletions
@@ -312,6 +312,9 @@ mod tests {
use super::PluginPackageReader; use super::PluginPackageReader;
use crate::services::plugin_package_fixtures::{sign_package_in_place, write_signed_package}; use crate::services::plugin_package_fixtures::{sign_package_in_place, write_signed_package};
use crate::services::plugin_signatures::{
PluginSignatureVerificationError, PluginSignatureVerifier, TrustedPluginSigningKeys,
};
#[test] #[test]
fn reads_verified_directory_package() -> Result<(), Box<dyn Error>> { fn reads_verified_directory_package() -> Result<(), Box<dyn Error>> {
@@ -325,6 +328,48 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn reads_package_with_trusted_signing_key() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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] #[test]
fn rejects_package_checksum_mismatch() -> Result<(), Box<dyn Error>> { fn rejects_package_checksum_mismatch() -> Result<(), Box<dyn Error>> {
let package = write_package("checksum", b"wasm component")?; let package = write_package("checksum", b"wasm component")?;
@@ -396,6 +441,13 @@ mod tests {
write_signed_package(temp_root()?.as_path(), name, component) write_signed_package(temp_root()?.as_path(), name, component)
} }
fn package_manifest(
path: &std::path::Path,
) -> Result<ely_domain::PluginManifest, Box<dyn Error>> {
let manifest = fs::read_to_string(path.join("plugin.toml"))?;
Ok(ely_domain::PluginManifest::from_toml(manifest.as_str())?)
}
fn temp_root() -> Result<PathBuf, Box<dyn Error>> { fn temp_root() -> Result<PathBuf, Box<dyn Error>> {
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
let path = std::env::temp_dir().join(format!("ely-plugin-package-{nanos}")); let path = std::env::temp_dir().join(format!("ely-plugin-package-{nanos}"));
@@ -1,4 +1,5 @@
use std::{ use std::{
collections::BTreeMap,
ffi::OsStr, ffi::OsStr,
fs, fs,
io::{self, Read}, io::{self, Read},
@@ -21,6 +22,18 @@ pub enum PluginSignatureVerificationError {
#[error("plugin signature verification failed for key {key_id}")] #[error("plugin signature verification failed for key {key_id}")]
VerificationFailed { key_id: String }, 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}")] #[error("failed to read plugin package entry {entry}: {path}")]
ReadFailed { entry: &'static str, path: PathBuf, source: io::Error }, ReadFailed { entry: &'static str, path: PathBuf, source: io::Error },
@@ -34,13 +47,66 @@ pub enum PluginSignatureVerificationError {
UnsupportedEntry { path: PathBuf }, UnsupportedEntry { path: PathBuf },
} }
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TrustedPluginSigningKeys {
keys: BTreeMap<String, String>,
}
pub struct PluginSignatureVerifier; pub struct PluginSignatureVerifier;
impl TrustedPluginSigningKeys {
pub fn from_entries<I, K, P>(entries: I) -> Result<Self, PluginSignatureVerificationError>
where
I: IntoIterator<Item = (K, P)>,
K: Into<String>,
P: Into<String>,
{
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<Self, PluginSignatureVerificationError> {
let signature = manifest.signature();
Self::from_entries([(signature.key_id().to_string(), signature.public_key().to_string())])
}
}
impl PluginSignatureVerifier { impl PluginSignatureVerifier {
pub fn verify( pub fn verify(
package_root: &Path, package_root: &Path,
manifest: &PluginManifest, manifest: &PluginManifest,
) -> Result<(), PluginSignatureVerificationError> { ) -> 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 key_id = manifest.signature().key_id().to_string();
let public_key = let public_key =
decode_hex_array::<32>(manifest.signature().public_key()).ok_or_else(|| { decode_hex_array::<32>(manifest.signature().public_key()).ok_or_else(|| {
@@ -90,6 +156,26 @@ pub(crate) fn signing_payload(
Ok(payload) Ok(payload)
} }
fn trusted_key_id(value: String) -> Result<String, PluginSignatureVerificationError> {
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<String, PluginSignatureVerificationError> {
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<String, PluginSignatureVerificationError> { fn package_files_hash(package_root: &Path) -> Result<String, PluginSignatureVerificationError> {
let mut entries = Vec::new(); let mut entries = Vec::new();
collect_signed_entries(package_root, Path::new(""), &mut entries)?; collect_signed_entries(package_root, Path::new(""), &mut entries)?;