Support trusted plugin signing keys
This commit is contained in:
@@ -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)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user