Support trusted plugin signing keys
This commit is contained in:
@@ -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<dyn Error>> {
|
||||
@@ -325,6 +328,48 @@ mod tests {
|
||||
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]
|
||||
fn rejects_package_checksum_mismatch() -> Result<(), Box<dyn Error>> {
|
||||
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<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>> {
|
||||
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!("ely-plugin-package-{nanos}"));
|
||||
|
||||
@@ -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<String, String>,
|
||||
}
|
||||
|
||||
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 {
|
||||
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<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> {
|
||||
let mut entries = Vec::new();
|
||||
collect_signed_entries(package_root, Path::new(""), &mut entries)?;
|
||||
|
||||
Reference in New Issue
Block a user