fix(plugins): trust official signing keys for offline installation
This commit is contained in:
5 files changed
+146
-9
No files matched your search
@@ -255,7 +255,7 @@ fn read_trust_document(root_dir: &Path) -> Result<PluginTrustDocument, String> {
|
||||
|
||||
impl PluginPackageInstaller {
|
||||
pub fn new(root_dir: PathBuf, app_version: impl Into<String>) -> Result<Self, String> {
|
||||
let trust_store = PluginTrustStore::load(&root_dir)?;
|
||||
let trust_store = super::marketplace::package_install_trust_store(&root_dir)?;
|
||||
Ok(Self { root_dir, app_version: app_version.into(), trust_store: Arc::new(trust_store) })
|
||||
}
|
||||
|
||||
@@ -1111,6 +1111,102 @@ mod tests {
|
||||
assert_eq!(result.signature, PluginSignatureStatus::Trusted { key_id: "sample-key".to_string() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_loads_official_keys_without_persisting_them() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let installer = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
|
||||
assert!(installer.trust_store.keys.contains_key("dbx-store-release-2026"));
|
||||
assert!(installer.trust_store.keys.contains_key("dbx-store-preview-2026"));
|
||||
assert!(PluginTrustStore::list_base64_keys(root.path()).unwrap().is_empty());
|
||||
assert!(!root.path().join(".trust").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_rejects_forged_official_signatures() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let installer = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
let forged_key = SigningKey::from_bytes(&[11u8; 32]);
|
||||
let package_path = root.path().join("forged.dbxp");
|
||||
std::fs::write(&package_path, package("1.0.0", Some((&forged_key, "dbx-store-release-2026")), false)).unwrap();
|
||||
|
||||
for policy in [PluginInstallPolicy::LocalSigned, PluginInstallPolicy::LocalDevelopment] {
|
||||
let error = installer.install_file(&package_path, policy).unwrap_err();
|
||||
assert_eq!(error, "Plugin package signature verification failed");
|
||||
}
|
||||
assert!(!root.path().join("sample.hello").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_preserves_custom_key_trust() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let signing_key = SigningKey::from_bytes(&[9u8; 32]);
|
||||
let public_key = base64::engine::general_purpose::STANDARD.encode(signing_key.verifying_key().as_bytes());
|
||||
PluginTrustStore::save_base64_key(root.path(), "sample-repository", &public_key).unwrap();
|
||||
let installer = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
let package_path = root.path().join("custom.dbxp");
|
||||
std::fs::write(&package_path, package("1.0.0", Some((&signing_key, "sample-repository")), false)).unwrap();
|
||||
|
||||
let installed = installer.install_file(&package_path, PluginInstallPolicy::LocalSigned).unwrap();
|
||||
|
||||
assert_eq!(installed.signature, PluginSignatureStatus::Trusted { key_id: "sample-repository".to_string() });
|
||||
assert!(installer.trust_store.keys.contains_key("dbx-store-release-2026"));
|
||||
assert_eq!(PluginTrustStore::list_base64_keys(root.path()).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_rejects_unknown_signed_packages() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let installer = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
let signing_key = SigningKey::from_bytes(&[9u8; 32]);
|
||||
let package = package("1.0.0", Some((&signing_key, "unknown-release")), false);
|
||||
|
||||
for policy in [PluginInstallPolicy::LocalSigned, PluginInstallPolicy::LocalDevelopment] {
|
||||
let error = installer.install_bytes(&package, policy).unwrap_err();
|
||||
assert_eq!(error, "Plugin package is signed by untrusted key 'unknown-release'");
|
||||
}
|
||||
assert!(!root.path().join("sample.hello").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_rejects_official_key_conflicts() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let forged_key = SigningKey::from_bytes(&[11u8; 32]);
|
||||
let public_key = base64::engine::general_purpose::STANDARD.encode(forged_key.verifying_key().as_bytes());
|
||||
PluginTrustStore::save_base64_key(root.path(), "dbx-store-release-2026", &public_key).unwrap();
|
||||
|
||||
let error = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67")
|
||||
.err()
|
||||
.expect("a custom key must not override an official signing key");
|
||||
|
||||
assert!(error.contains("'dbx-store-release-2026' already exists with a different public key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_accepts_matching_official_keys() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let installer = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
let official_key = installer.trust_store.keys.get("dbx-store-release-2026").unwrap();
|
||||
let public_key = base64::engine::general_purpose::STANDARD.encode(official_key.as_bytes());
|
||||
PluginTrustStore::save_base64_key(root.path(), "dbx-store-release-2026", &public_key).unwrap();
|
||||
|
||||
let reloaded = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
|
||||
assert_eq!(reloaded.trust_store.keys.get("dbx-store-release-2026"), Some(official_key));
|
||||
assert_eq!(PluginTrustStore::list_base64_keys(root.path()).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_installer_enforces_unsigned_policy() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let installer = PluginPackageInstaller::new(root.path().to_path_buf(), "0.5.67").unwrap();
|
||||
let unsigned = package("1.0.0", None, false);
|
||||
|
||||
assert!(installer.install_bytes(&unsigned, PluginInstallPolicy::LocalSigned).is_err());
|
||||
let installed = installer.install_bytes(&unsigned, PluginInstallPolicy::LocalDevelopment).unwrap();
|
||||
assert_eq!(installed.signature, PluginSignatureStatus::Unsigned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marketplace_expectation_binds_manifest_and_signing_identity() {
|
||||
let manifest: PluginManifest = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -445,9 +445,7 @@ impl PluginMarketplace {
|
||||
bytes.extend_from_slice(&chunk);
|
||||
on_progress(bytes.len() as u64, total);
|
||||
}
|
||||
let trust_store = url_install_trust_store(&self.root_dir)?;
|
||||
PluginPackageInstaller::with_trust_store(self.root_dir.clone(), self.app_version.clone(), trust_store)
|
||||
.install_bytes(&bytes, policy)
|
||||
PluginPackageInstaller::new(self.root_dir.clone(), self.app_version.clone())?.install_bytes(&bytes, policy)
|
||||
}
|
||||
|
||||
async fn download_limited(&self, url: Url, max_bytes: usize, label: &str) -> Result<Vec<u8>, String> {
|
||||
@@ -719,11 +717,11 @@ fn marketplace_trust_store(root_dir: &Path, kind: PluginRepositoryKind) -> Resul
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Trust store for direct URL installs: the user's trusted keys plus the
|
||||
/// Trust store for local file and direct URL installs: the user's trusted keys plus the
|
||||
/// built-in official DBX Marketplace keys. A user-saved key that collides with
|
||||
/// a builtin key id but carries a different public key is a rotation conflict
|
||||
/// and fails the install instead of silently overriding the builtin key.
|
||||
pub fn url_install_trust_store(root_dir: &Path) -> Result<PluginTrustStore, String> {
|
||||
pub(super) fn package_install_trust_store(root_dir: &Path) -> Result<PluginTrustStore, String> {
|
||||
let mut keys = PluginTrustStore::list_base64_keys(root_dir)?
|
||||
.into_iter()
|
||||
.map(|key| (key.key_id, key.public_key))
|
||||
@@ -732,7 +730,7 @@ pub fn url_install_trust_store(root_dir: &Path) -> Result<PluginTrustStore, Stri
|
||||
if let Some(existing) = keys.get(&key_id) {
|
||||
if existing.trim() != public_key {
|
||||
return Err(format!(
|
||||
"Trusted plugin key '{key_id}' already exists with a different public key; remove it before installing official store packages from a URL"
|
||||
"Trusted plugin key '{key_id}' already exists with a different public key; remove it before installing official store packages"
|
||||
));
|
||||
}
|
||||
continue;
|
||||
@@ -742,6 +740,10 @@ pub fn url_install_trust_store(root_dir: &Path) -> Result<PluginTrustStore, Stri
|
||||
PluginTrustStore::from_base64_keys(keys)
|
||||
}
|
||||
|
||||
pub fn url_install_trust_store(root_dir: &Path) -> Result<PluginTrustStore, String> {
|
||||
package_install_trust_store(root_dir)
|
||||
}
|
||||
|
||||
fn parse_http_url(raw: &str, label: &str) -> Result<Url, String> {
|
||||
let url = Url::parse(raw).map_err(|error| format!("Invalid {label}: {error}"))?;
|
||||
ensure_http_url(url, label)
|
||||
@@ -1013,6 +1015,14 @@ mod tests {
|
||||
assert!(!marketplace_trust_store(root.path(), PluginRepositoryKind::Official).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_repositories_do_not_implicitly_trust_official_keys() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
for kind in [PluginRepositoryKind::Custom, PluginRepositoryKind::Enterprise] {
|
||||
assert!(marketplace_trust_store(root.path(), kind).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn isolates_repository_fetch_failures() {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
Reference in new issue
Block a user