fix(community): 原子确认多密钥来源信任

This commit is contained in:
2026-09-08 22:12:49 +08:00
parent 86b9d06dcc
commit 7bdc66ef75
9 changed files with 188 additions and 5 deletions
+1
View File
@@ -18,6 +18,7 @@ fn main() {
"host_capabilities",
"extension_trust_review",
"extension_trust_confirm",
"extension_trust_confirm_group",
"extension_install_preview",
"extension_stage",
"extension_stage_prepare",
+2 -1
View File
@@ -57,6 +57,7 @@
"allow-extension-stage-prepare",
"allow-extension-stage-cancel",
"allow-extension-stage-status",
"allow-extension-staged"
"allow-extension-staged",
"allow-extension-trust-confirm-group"
]
}
@@ -0,0 +1,11 @@
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-extension-trust-confirm-group"
description = "Enables the extension_trust_confirm_group command without any pre-configured scope."
commands.allow = ["extension_trust_confirm_group"]
[[permission]]
identifier = "deny-extension-trust-confirm-group"
description = "Denies the extension_trust_confirm_group command without any pre-configured scope."
commands.deny = ["extension_trust_confirm_group"]
@@ -247,10 +247,88 @@ pub async fn extension_staged(
.map_err(|_| "EXTENSION_LIST_FAILED".to_string())?
}
#[tauri::command]
pub fn extension_trust_confirm_group(
window: WebviewWindow,
host: State<'_, Host>,
requests: Vec<Confirmation>,
) -> Result<Vec<String>, String> {
main_window(&window)?;
trust_confirm_group(&host, requests)
}
fn trust_confirm_group(host: &Host, requests: Vec<Confirmation>) -> Result<Vec<String>, String> {
if requests.is_empty() || requests.len() > 64 {
return Err("EXTENSION_TRUST_INVALID".into());
}
let mut reviews = host.extension_reviews.0.lock().map_err(|_| "HOST_BUSY")?;
let mut ids = std::collections::BTreeSet::new();
let mut proposals = Vec::new();
for request in &requests {
if !ids.insert(&request.review_id) {
return Err("EXTENSION_TRUST_INVALID".into());
}
let review = reviews
.get(&request.review_id)
.ok_or("EXTENSION_REVIEW_EXPIRED")?;
if review.expires <= Instant::now() {
return Err("EXTENSION_REVIEW_EXPIRED".into());
}
if review.fingerprint != request.fingerprint {
return Err("EXTENSION_TRUST_CONFIRMATION".into());
}
proposals.push((
review.setting.clone(),
review.expected.clone(),
review.fingerprint.clone(),
));
}
let revisions = store(host, |s| s.confirm_trust_group(&proposals))?;
for request in requests {
reviews.remove(&request.review_id);
}
Ok(revisions)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn failed_group_does_not_consume_valid_review_then_success_consumes_all() {
let temp = tempfile::tempdir().unwrap();
let host = Host::default();
*host.extensions.lock().unwrap() = Some(ExtensionStore::open(temp.path()).unwrap());
let mut setting = TrustSetting {
source: "https://catalog.example/".into(),
source_id: "catalog".into(),
namespace: "examples".into(),
key_id: "one".into(),
public_key: ed25519_dalek::SigningKey::from_bytes(&[7; 32])
.verifying_key()
.to_bytes(),
enabled: true,
};
let first = trust_review(&host, setting.clone()).unwrap();
setting.key_id = "two".into();
let second = trust_review(&host, setting).unwrap();
let input = |value: &Value| Confirmation {
review_id: value["review_id"].as_str().unwrap().into(),
fingerprint: value["fingerprint"].as_str().unwrap().into(),
};
let mut wrong = input(&second);
wrong.fingerprint = "wrong".into();
assert!(trust_confirm_group(&host, vec![input(&first), wrong]).is_err());
assert_eq!(host.extension_reviews.0.lock().unwrap().len(), 2);
assert!(store(&host, |s| s.trust_setting(
"https://catalog.example/",
"examples",
"one"
))
.unwrap()
.is_none());
trust_confirm_group(&host, vec![input(&first), input(&second)]).unwrap();
assert!(host.extension_reviews.0.lock().unwrap().is_empty());
}
#[test]
fn review_nonce_expiry_consumption_and_stale_setting_are_enforced() {
let temp = tempfile::tempdir().unwrap();
let host = Host::default();
+85
View File
@@ -374,6 +374,39 @@ impl ExtensionStore {
params![setting.source,setting.namespace,setting.key_id,serde_json::to_string(setting).unwrap(),revision])?;
Ok(revision)
}
pub fn confirm_trust_group(
&mut self,
proposals: &[(TrustSetting, Option<String>, String)],
) -> Result<Vec<String>> {
if proposals.is_empty() || proposals.len() > 64 {
return Err(HostError::new("EXTENSION_TRUST_INVALID"));
}
let mut identities = std::collections::BTreeSet::new();
for (setting, _, _) in proposals {
if setting.source != proposals[0].0.source
|| setting.source_id != proposals[0].0.source_id
|| !identities.insert((&setting.namespace, &setting.key_id))
{
return Err(HostError::new("EXTENSION_TRUST_INVALID"));
}
}
self.db
.execute_batch("SAVEPOINT trust_confirmation_group")?;
let result = (|| {
let mut revisions = Vec::new();
for (setting, expected, confirmed) in proposals {
revisions.push(self.confirm_trust(setting, expected.as_deref(), confirmed)?);
}
self.db.execute_batch("RELEASE trust_confirmation_group")?;
Ok(revisions)
})();
if result.is_err() {
self.db.execute_batch(
"ROLLBACK TO trust_confirmation_group; RELEASE trust_confirmation_group",
)?;
}
result
}
fn block_identity(
source_url: &str,
release: &Release,
@@ -1112,6 +1145,58 @@ mod tests {
}
}
#[test]
fn grouped_trust_confirmation_rolls_back_earlier_keys_on_later_conflict() {
let temp = tempfile::tempdir().unwrap();
let (_, _, key) = fixture();
let mut store = ExtensionStore::open(temp.path()).unwrap();
let first = TrustSetting {
source: "https://catalog.example/".into(),
source_id: "catalog".into(),
namespace: "examples".into(),
key_id: "first".into(),
public_key: key,
enabled: true,
};
let mut second = first.clone();
second.key_id = "second".into();
store
.confirm_trust(&second, None, &second.fingerprint().unwrap())
.unwrap();
second.enabled = false;
let proposals = vec![
(first.clone(), None, first.fingerprint().unwrap()),
(second.clone(), None, second.fingerprint().unwrap()),
];
assert!(store.confirm_trust_group(&proposals).is_err());
drop(store);
let mut store = ExtensionStore::open(temp.path()).unwrap();
assert!(store
.trust_setting(&first.source, &first.namespace, &first.key_id)
.unwrap()
.is_none());
let old = store
.trust_setting(&second.source, &second.namespace, &second.key_id)
.unwrap()
.unwrap();
assert!(old.enabled);
let mut proposals = proposals;
proposals[1].1 = Some(old.fingerprint().unwrap());
store.confirm_trust_group(&proposals).unwrap();
assert!(store
.trust_setting(&first.source, &first.namespace, &first.key_id)
.unwrap()
.is_some());
assert!(
!store
.trust_setting(&second.source, &second.namespace, &second.key_id)
.unwrap()
.unwrap()
.enabled
);
let duplicate = vec![proposals[0].clone(), proposals[0].clone()];
assert!(store.confirm_trust_group(&duplicate).is_err());
}
#[test]
fn cancelled_stage_reports_durable_commit_race_truthfully() {
let (release, archive, key) = fixture();
for boundary in ["object_stored", "receipt_recorded", "committed"] {
+1
View File
@@ -850,6 +850,7 @@ fn main() {
host_capabilities,
extension_trust_review,
extension_trust_confirm,
extension_trust_confirm_group,
extension_install_preview,
extension_stage,
extension_stage_prepare,