fix(plugins): trust official signing keys for offline installation

This commit is contained in:
t8y2 committed 2026-09-17 18:03:47 +08:00
1 parent 65b820e6a9
commit fb25a66d7a
5 files changed
+146 -9

No files matched your search

+97 -1
View File
@@ -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!({
+16 -6
View File
@@ -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};