feat(community): 通过 Host 审阅确认桌面来源信任
This commit is contained in:
@@ -21,7 +21,7 @@ describe('社区 Python / TypeScript 签名契约', () => {
|
||||
await expect(verifyRelease(release, { ...vector.key, revoked: true }, bytes)).rejects.toThrow('撤回')
|
||||
})
|
||||
it('不携带凭据、拒绝重定向并支持取消', async () => {
|
||||
const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ schema_version: 1, keys: [] })))
|
||||
const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ schema_version: 1, source_id: "fixture", keys: [] })))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
await discoverKeys('https://example.org')
|
||||
expect(fetch.mock.calls[0]?.[1]).toMatchObject({ credentials: 'omit', redirect: 'error', referrerPolicy: 'no-referrer' })
|
||||
|
||||
@@ -47,11 +47,14 @@ async function download(source: CommunitySource, path: string, maxSize: number,
|
||||
} finally { clearTimeout(timeout); signal?.removeEventListener('abort', abort) }
|
||||
}
|
||||
|
||||
export async function discoverKeys(url: string, signal?: AbortSignal): Promise<CommunityKey[]> {
|
||||
export async function discoverSource(url: string, signal?: AbortSignal): Promise<{ source_id: string; keys: CommunityKey[] }> {
|
||||
const source: CommunitySource = { id: 'candidate', url, enabled: true, keys: [] }
|
||||
const value = JSON.parse(new TextDecoder().decode(await download(source, '/catalog/v1/sources', 1024 * 1024, signal)))
|
||||
if (value.schema_version !== 1 || !Array.isArray(value.keys)) throw new Error('不支持的社区来源协议')
|
||||
return value.keys
|
||||
if (value.schema_version !== 1 || typeof value.source_id !== 'string' || !value.source_id || !Array.isArray(value.keys) || value.keys.length > 64) throw new Error('不支持的社区来源协议')
|
||||
return { source_id: value.source_id, keys: value.keys }
|
||||
}
|
||||
export async function discoverKeys(url: string, signal?: AbortSignal): Promise<CommunityKey[]> {
|
||||
return (await discoverSource(url, signal)).keys
|
||||
}
|
||||
|
||||
export async function fetchCatalog(source: CommunitySource, q = '', kind = '', signal?: AbortSignal): Promise<CommunityCatalog> {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
const native = vi.hoisted(() => ({ invoke: vi.fn() }))
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: native.invoke }))
|
||||
import { confirmTrust, reviewTrust } from './extensionTrustService'
|
||||
beforeEach(() => native.invoke.mockReset())
|
||||
const key = { key_id: 'key', namespace: 'examples', public_key: btoa('a'.repeat(32)), revoked: false }
|
||||
it('reviews normalized source and confirms only Host-issued id and fingerprint', async () => {
|
||||
native.invoke.mockResolvedValueOnce({ review_id: 'review', fingerprint: 'digest', previous: null })
|
||||
const reviews = await reviewTrust('https://catalog.example', 'catalog', [key], true)
|
||||
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' } })
|
||||
})
|
||||
it('rejects invalid and revoked input before invoking Host', async () => {
|
||||
await expect(reviewTrust('http://example.com', 'catalog', [key], true)).rejects.toThrow()
|
||||
await expect(reviewTrust('https://example.com', 'catalog', [{ ...key, revoked: true }], true)).rejects.toThrow()
|
||||
await expect(reviewTrust('https://example.com', 'catalog', [{ ...key, public_key: 'bad' }], true)).rejects.toThrow()
|
||||
await expect(confirmTrust([])).rejects.toThrow()
|
||||
expect(native.invoke).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { CommunityKey } from '@/contracts/community'
|
||||
|
||||
export interface TrustSetting {
|
||||
source: string; source_id: string; namespace: string; key_id: string
|
||||
public_key: number[]; enabled: boolean
|
||||
}
|
||||
export interface TrustReview {
|
||||
review_id: string; fingerprint: string; previous: TrustSetting | null; proposed: TrustSetting
|
||||
}
|
||||
export async function reviewTrust(url: string, sourceId: string, keys: CommunityKey[], enabled: boolean): Promise<TrustReview[]> {
|
||||
const source = new URL(url)
|
||||
if (source.protocol !== 'https:' || source.username || source.password || source.search || source.hash) throw new Error('桌面信任来源必须使用 HTTPS')
|
||||
source.pathname = `${source.pathname.replace(/\/+$/, '')}/`
|
||||
if (!keys.length || keys.length > 64) throw new Error('来源公钥数量必须为 1–64')
|
||||
const settings = keys.map(key => {
|
||||
const publicKey = Array.from(atob(key.public_key), c => c.charCodeAt(0))
|
||||
if (publicKey.length !== 32 || (enabled && key.revoked)) throw new Error('公钥无效或已撤销')
|
||||
return { source: source.toString(), source_id: sourceId, namespace: key.namespace, key_id: key.key_id, public_key: publicKey, enabled }
|
||||
})
|
||||
const reviews: TrustReview[] = []
|
||||
for (const setting of settings) reviews.push(await invoke<TrustReview>('extension_trust_review', { setting }))
|
||||
return reviews
|
||||
}
|
||||
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 } })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user