fix(community): 原子确认多密钥来源信任
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,7 +10,7 @@ it('reviews normalized source and confirms only Host-issued id and fingerprint',
|
||||
expect(native.invoke).toHaveBeenCalledWith('extension_trust_review', { setting: { source: 'https://catalog.example/', source_id: 'catalog', key_id: 'key', namespace: 'examples', public_key: Array(32).fill(97), enabled: true } })
|
||||
expect(native.invoke).toHaveBeenCalledTimes(1)
|
||||
await confirmTrust(reviews)
|
||||
expect(native.invoke).toHaveBeenLastCalledWith('extension_trust_confirm', { request: { review_id: 'review', fingerprint: 'digest' } })
|
||||
expect(native.invoke).toHaveBeenLastCalledWith('extension_trust_confirm_group', { requests: [{ review_id: 'review', fingerprint: 'digest' }] })
|
||||
})
|
||||
it('rejects invalid and revoked input before invoking Host', async () => {
|
||||
await expect(reviewTrust('http://example.com', 'catalog', [key], true)).rejects.toThrow()
|
||||
|
||||
@@ -24,7 +24,5 @@ export async function reviewTrust(url: string, sourceId: string, keys: Community
|
||||
}
|
||||
export async function confirmTrust(reviews: TrustReview[]): Promise<void> {
|
||||
if (!reviews.length || reviews.length > 64) throw new Error('请重新检查来源后确认')
|
||||
for (const review of reviews) {
|
||||
await invoke('extension_trust_confirm', { request: { review_id: review.review_id, fingerprint: review.fingerprint } })
|
||||
}
|
||||
await invoke('extension_trust_confirm_group', { requests: reviews.map(review => ({ review_id: review.review_id, fingerprint: review.fingerprint })) })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user