Restore remote Upgrade to Hub button web component
This commit is contained in:
parent
77b1282570
commit
a01c73d506
18 changed files with 1642 additions and 303 deletions
|
|
@ -1,12 +1,13 @@
|
|||
import { waitFor } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
import HubDashboard, { resetCache } from './HubDashboard'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
import { renderWithProviders } from 'utils/test'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('./workers/scriptVerification', () => ({
|
||||
vi.mock('utils/workers/scriptVerification', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
|
|
@ -34,7 +35,6 @@ describe('HubDashboard demo', () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock URL.createObjectURL
|
||||
mockCreateObjectURL = vi.fn(() => 'blob:mock-url')
|
||||
globalThis.URL.createObjectURL = mockCreateObjectURL
|
||||
})
|
||||
|
|
@ -45,7 +45,6 @@ describe('HubDashboard demo', () => {
|
|||
|
||||
describe('without cache', () => {
|
||||
beforeEach(() => {
|
||||
// Reset cache before each test suites
|
||||
resetCache()
|
||||
})
|
||||
|
||||
|
|
@ -130,6 +129,7 @@ describe('HubDashboard demo', () => {
|
|||
expect(mockVerifyScriptSignature).toHaveBeenCalledWith(
|
||||
'https://assets.traefik.io/hub-ui-demo.js',
|
||||
'https://assets.traefik.io/hub-ui-demo.js.sig',
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { useMemo, useEffect, useState } from 'react'
|
|||
import { Helmet } from 'react-helmet-async'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
import verifySignature from '../../utils/workers/scriptVerification'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
|
||||
import { SpinnerLoader } from 'components/SpinnerLoader'
|
||||
import { useIsDarkMode } from 'hooks/use-theme'
|
||||
|
|
@ -42,7 +44,7 @@ const HubDashboard = ({ path }: { path: string }) => {
|
|||
setVerificationInProgress(true)
|
||||
|
||||
try {
|
||||
const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`)
|
||||
const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`, PUBLIC_KEY)
|
||||
|
||||
if (!verified || !content) {
|
||||
setScriptError(true)
|
||||
|
|
|
|||
1
webui/src/pages/hub-demo/constants.ts
Normal file
1
webui/src/pages/hub-demo/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg='
|
||||
|
|
@ -3,9 +3,10 @@ import { ReactNode } from 'react'
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { useHubDemo } from './use-hub-demo'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
vi.mock('./workers/scriptVerification', () => ({
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('utils/workers/scriptVerification', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
|
||||
import HubDashboard from 'pages/hub-demo/HubDashboard'
|
||||
import { ApiIcon, DashboardIcon, GatewayIcon, PortalIcon } from 'pages/hub-demo/icons'
|
||||
import verifySignature from 'pages/hub-demo/workers/scriptVerification'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
const ROUTES_MANIFEST_URL = 'https://traefik.github.io/hub-ui-demo-app/config/routes.json'
|
||||
|
||||
|
|
@ -20,7 +22,11 @@ const useHubDemoRoutesManifest = (): HubDemo.Manifest | null => {
|
|||
useEffect(() => {
|
||||
const fetchManifest = async () => {
|
||||
try {
|
||||
const { verified, scriptContent } = await verifySignature(ROUTES_MANIFEST_URL, `${ROUTES_MANIFEST_URL}.sig`)
|
||||
const { verified, scriptContent } = await verifySignature(
|
||||
ROUTES_MANIFEST_URL,
|
||||
`${ROUTES_MANIFEST_URL}.sig`,
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
|
||||
if (!verified || !scriptContent) {
|
||||
setManifest(null)
|
||||
|
|
|
|||
|
|
@ -1,156 +0,0 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import verifySignature from './scriptVerification'
|
||||
|
||||
describe('Script Signature Verification - Integration Tests', () => {
|
||||
let fetchMock: ReturnType<typeof vi.fn>
|
||||
|
||||
const SCRIPT_URL = 'https://example.com/script.js'
|
||||
const SIGNATURE_URL = 'https://example.com/script.js.sig'
|
||||
const TEST_PUBLIC_KEY = 'MCowBQYDK2VwAyEAWH71OHphISjNK3mizCR/BawiDxc6IXT1vFHpBcxSIA0='
|
||||
const VALID_SCRIPT = "console.log('Hello from verified script!');"
|
||||
const VALID_SIGNATURE_HEX =
|
||||
'04c90fcd35caaf3cf4582a2767345f8cd9f6519e1ce79ebaeedbe0d5f671d762d1aa8ec258831557e2de0e47f224883f84eb5a0f22ec18eb7b8c48de3096d000'
|
||||
const CORRUPTED_SCRIPT = "console.log('Malicious code injected!');"
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
fetchMock = vi.fn()
|
||||
globalThis.fetch = fetchMock
|
||||
})
|
||||
|
||||
it('should verify a valid script with correct signature through real worker', async () => {
|
||||
fetchMock.mockImplementation((url: string) => {
|
||||
if (url === SCRIPT_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(VALID_SCRIPT, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/javascript' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (url === SIGNATURE_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(VALID_SIGNATURE_HEX, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected URL'))
|
||||
})
|
||||
|
||||
const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(SCRIPT_URL)
|
||||
expect(fetchMock).toHaveBeenCalledWith(SIGNATURE_URL)
|
||||
expect(result.verified).toBe(true)
|
||||
expect(result.scriptContent).toBeDefined()
|
||||
}, 15000)
|
||||
|
||||
it('should reject a corrupted script with mismatched signature', async () => {
|
||||
fetchMock.mockImplementation((url: string) => {
|
||||
if (url === SCRIPT_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(CORRUPTED_SCRIPT, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/javascript' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (url === SIGNATURE_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(VALID_SIGNATURE_HEX, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected URL'))
|
||||
})
|
||||
|
||||
const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(SCRIPT_URL)
|
||||
expect(fetchMock).toHaveBeenCalledWith(SIGNATURE_URL)
|
||||
expect(result.verified).toBe(false)
|
||||
expect(result.scriptContent).toBeUndefined()
|
||||
}, 15000)
|
||||
|
||||
it('should reject script with invalid signature format', async () => {
|
||||
fetchMock.mockImplementation((url: string) => {
|
||||
if (url === SCRIPT_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(VALID_SCRIPT, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/javascript' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (url === SIGNATURE_URL) {
|
||||
return Promise.resolve(
|
||||
new Response('not-a-valid-signature', {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected URL'))
|
||||
})
|
||||
|
||||
const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY)
|
||||
|
||||
expect(result.verified).toBe(false)
|
||||
expect(result.scriptContent).toBeUndefined()
|
||||
}, 15000)
|
||||
|
||||
it('should reject script with wrong public key', async () => {
|
||||
const WRONG_PUBLIC_KEY = 'MCowBQYDK2VwAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='
|
||||
|
||||
fetchMock.mockImplementation((url: string) => {
|
||||
if (url === SCRIPT_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(VALID_SCRIPT, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/javascript' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (url === SIGNATURE_URL) {
|
||||
return Promise.resolve(
|
||||
new Response(VALID_SIGNATURE_HEX, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected URL'))
|
||||
})
|
||||
|
||||
const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, WRONG_PUBLIC_KEY)
|
||||
|
||||
expect(result.verified).toBe(false)
|
||||
expect(result.scriptContent).toBeUndefined()
|
||||
}, 15000)
|
||||
|
||||
it('should handle network failures when fetching script', async () => {
|
||||
fetchMock.mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(null, {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY)
|
||||
|
||||
expect(result.verified).toBe(false)
|
||||
expect(result.scriptContent).toBeUndefined()
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
}, 15000)
|
||||
})
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import verifySignature from './scriptVerification'
|
||||
|
||||
class MockWorker {
|
||||
onmessage: ((event: MessageEvent) => void) | null = null
|
||||
onerror: ((error: ErrorEvent) => void) | null = null
|
||||
postMessage = vi.fn()
|
||||
terminate = vi.fn()
|
||||
|
||||
simulateMessage(data: unknown) {
|
||||
if (this.onmessage) {
|
||||
this.onmessage(new MessageEvent('message', { data }))
|
||||
}
|
||||
}
|
||||
|
||||
simulateError(error: Error) {
|
||||
if (this.onerror) {
|
||||
this.onerror(new ErrorEvent('error', { error, message: error.message }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('verifySignature', () => {
|
||||
let mockWorkerInstance: MockWorker
|
||||
let originalWorker: typeof Worker
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
originalWorker = globalThis.Worker
|
||||
|
||||
mockWorkerInstance = new MockWorker()
|
||||
|
||||
globalThis.Worker = class extends EventTarget {
|
||||
constructor() {
|
||||
super()
|
||||
return mockWorkerInstance as any
|
||||
}
|
||||
} as any
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.Worker = originalWorker
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should return true when verification succeeds', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scriptUrl: scriptPath,
|
||||
signatureUrl: signaturePath,
|
||||
requestId: expect.any(String),
|
||||
}),
|
||||
)
|
||||
|
||||
const mockScriptContent = new ArrayBuffer(100)
|
||||
mockWorkerInstance.simulateMessage({
|
||||
success: true,
|
||||
verified: true,
|
||||
error: null,
|
||||
scriptContent: mockScriptContent,
|
||||
})
|
||||
|
||||
const result = await promise
|
||||
|
||||
expect(result).toEqual({ verified: true, scriptContent: mockScriptContent })
|
||||
expect(mockWorkerInstance.terminate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return false when verification fails', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
mockWorkerInstance.simulateMessage({
|
||||
success: false,
|
||||
verified: false,
|
||||
error: 'Signature verification failed',
|
||||
})
|
||||
|
||||
const result = await promise
|
||||
|
||||
expect(result).toEqual({ verified: false })
|
||||
expect(mockWorkerInstance.terminate).toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Worker verification failed:', 'Signature verification failed')
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should return false when worker throws an error', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// Simulate worker onerror event
|
||||
const error = new Error('Worker crashed')
|
||||
mockWorkerInstance.simulateError(error)
|
||||
|
||||
const result = await promise
|
||||
|
||||
expect(result).toEqual({ verified: false })
|
||||
expect(mockWorkerInstance.terminate).toHaveBeenCalled()
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Worker error:', expect.any(ErrorEvent))
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
export interface VerificationResult {
|
||||
verified: boolean
|
||||
scriptContent?: ArrayBuffer
|
||||
}
|
||||
|
||||
const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg='
|
||||
|
||||
async function verifySignature(
|
||||
contentPath: string,
|
||||
signaturePath: string,
|
||||
publicKey: string = PUBLIC_KEY,
|
||||
): Promise<VerificationResult> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = Math.random().toString(36).substring(2)
|
||||
const worker = new Worker(new URL('./scriptVerificationWorker.ts', import.meta.url), { type: 'module' })
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
worker.terminate()
|
||||
console.error('Script verification timeout')
|
||||
resolve({ verified: false })
|
||||
}, 30000)
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
clearTimeout(timeout)
|
||||
worker.terminate()
|
||||
|
||||
const { success, verified, error, scriptContent } = event.data
|
||||
|
||||
if (!success) {
|
||||
console.error('Worker verification failed:', error)
|
||||
resolve({ verified: false })
|
||||
return
|
||||
}
|
||||
|
||||
resolve({
|
||||
verified: verified === true,
|
||||
scriptContent: verified ? scriptContent : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
worker.onerror = (error) => {
|
||||
clearTimeout(timeout)
|
||||
worker.terminate()
|
||||
console.error('Worker error:', error)
|
||||
resolve({ verified: false })
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
requestId,
|
||||
scriptUrl: contentPath,
|
||||
signatureUrl: signaturePath,
|
||||
publicKey,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default verifySignature
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
// Script verification worker
|
||||
// Runs in isolated context for secure verification
|
||||
|
||||
import { verify } from '@noble/ed25519'
|
||||
import * as ed25519 from '@noble/ed25519'
|
||||
import { sha512 } from '@noble/hashes/sha2.js'
|
||||
|
||||
// Set up SHA-512 for @noble/ed25519 v3.x
|
||||
ed25519.hashes.sha512 = sha512
|
||||
ed25519.hashes.sha512Async = (m) => Promise.resolve(sha512(m))
|
||||
|
||||
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
try {
|
||||
// @ts-expect-error - fromBase64 is not yet in all TypeScript lib definitions
|
||||
const bytes = Uint8Array.fromBase64(base64)
|
||||
return bytes.buffer
|
||||
} catch {
|
||||
// Fallback for browsers without Uint8Array.fromBase64()
|
||||
const binaryString = atob(base64)
|
||||
const bytes = new Uint8Array(binaryString.length)
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
return bytes.buffer
|
||||
}
|
||||
}
|
||||
|
||||
function extractEd25519PublicKey(spkiBytes: Uint8Array): Uint8Array {
|
||||
if (spkiBytes.length !== 44) {
|
||||
throw new Error('Invalid SPKI length for Ed25519')
|
||||
}
|
||||
return spkiBytes.slice(-32)
|
||||
}
|
||||
|
||||
async function importPublicKeyWebCrypto(publicKey: string): Promise<CryptoKey> {
|
||||
const publicKeyBuffer = base64ToArrayBuffer(publicKey)
|
||||
|
||||
return await crypto.subtle.importKey(
|
||||
'spki',
|
||||
publicKeyBuffer,
|
||||
{
|
||||
name: 'Ed25519',
|
||||
},
|
||||
false,
|
||||
['verify'],
|
||||
)
|
||||
}
|
||||
|
||||
async function verifyWithWebCrypto(
|
||||
publicKey: string,
|
||||
scriptBuffer: ArrayBuffer,
|
||||
signatureBuffer: ArrayBuffer,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const cryptoPublicKey = await importPublicKeyWebCrypto(publicKey)
|
||||
|
||||
return await crypto.subtle.verify('Ed25519', cryptoPublicKey, signatureBuffer, scriptBuffer)
|
||||
} catch (error) {
|
||||
console.log('Web Crypto verification failed:', error instanceof Error ? error.message : 'Unknown error')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function parseSignature(signatureBuffer: ArrayBuffer): Uint8Array {
|
||||
const signatureBytes = new Uint8Array(signatureBuffer)
|
||||
|
||||
// If already 64 bytes, assume it's raw binary
|
||||
if (signatureBytes.length === 64) {
|
||||
return signatureBytes
|
||||
}
|
||||
|
||||
// Try to parse as text (base64 or hex)
|
||||
const signatureText = new TextDecoder().decode(signatureBytes).trim()
|
||||
|
||||
// base64 decoding
|
||||
try {
|
||||
const base64Decoded = new Uint8Array(base64ToArrayBuffer(signatureText))
|
||||
if (base64Decoded.length === 64) {
|
||||
return base64Decoded
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
// hex decoding
|
||||
if (signatureText.length === 128 && /^[0-9a-fA-F]+$/.test(signatureText)) {
|
||||
try {
|
||||
// @ts-expect-error - fromHex is not yet in all TypeScript lib definitions
|
||||
return Uint8Array.fromHex(signatureText)
|
||||
} catch {
|
||||
// Fallback for browsers without Uint8Array.fromHex()
|
||||
const hexDecoded = new Uint8Array(64)
|
||||
for (let i = 0; i < 64; i++) {
|
||||
hexDecoded[i] = parseInt(signatureText.slice(i * 2, i * 2 + 2), 16)
|
||||
}
|
||||
return hexDecoded
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unable to parse signature format.`)
|
||||
}
|
||||
|
||||
async function verifyWithNoble(
|
||||
publicKey: string,
|
||||
scriptBuffer: ArrayBuffer,
|
||||
signatureBuffer: ArrayBuffer,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const publicKeySpki = new Uint8Array(base64ToArrayBuffer(publicKey))
|
||||
const publicKeyRaw = extractEd25519PublicKey(publicKeySpki)
|
||||
|
||||
const scriptBytes = new Uint8Array(scriptBuffer)
|
||||
const signatureBytes = parseSignature(signatureBuffer)
|
||||
|
||||
return verify(signatureBytes, scriptBytes, publicKeyRaw)
|
||||
} catch (error) {
|
||||
console.log('Noble verification failed:', error instanceof Error ? error.message : 'Unknown error')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = async function (event) {
|
||||
const { requestId, scriptUrl, signatureUrl, publicKey } = event.data
|
||||
|
||||
try {
|
||||
const [scriptResponse, signatureResponse] = await Promise.all([fetch(scriptUrl), fetch(signatureUrl)])
|
||||
|
||||
if (!scriptResponse.ok || !signatureResponse.ok) {
|
||||
self.postMessage({
|
||||
requestId,
|
||||
success: false,
|
||||
verified: false,
|
||||
error: `Failed to fetch files. Script: ${scriptResponse.status} ${scriptResponse.statusText}, Signature: ${signatureResponse.status} ${signatureResponse.statusText}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const [scriptBuffer, signatureBuffer] = await Promise.all([
|
||||
scriptResponse.arrayBuffer(),
|
||||
signatureResponse.arrayBuffer(),
|
||||
])
|
||||
|
||||
// Try Web Crypto API first, fallback to Noble if it fails
|
||||
let verified = await verifyWithWebCrypto(publicKey, scriptBuffer, signatureBuffer)
|
||||
|
||||
if (!verified) {
|
||||
verified = await verifyWithNoble(publicKey, scriptBuffer, signatureBuffer)
|
||||
}
|
||||
|
||||
// If verified, include script content to avoid re-downloading
|
||||
let scriptContent: ArrayBuffer | undefined
|
||||
if (verified) {
|
||||
scriptContent = scriptBuffer
|
||||
}
|
||||
|
||||
// Send message with transferable ArrayBuffer for efficiency
|
||||
const message = {
|
||||
requestId,
|
||||
success: true,
|
||||
verified,
|
||||
scriptSize: scriptBuffer.byteLength,
|
||||
signatureSize: signatureBuffer.byteLength,
|
||||
scriptContent,
|
||||
}
|
||||
|
||||
if (scriptContent) {
|
||||
self.postMessage(message, { transfer: [scriptContent] })
|
||||
} else {
|
||||
self.postMessage(message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Worker] Verification error:', error)
|
||||
self.postMessage({
|
||||
requestId,
|
||||
success: false,
|
||||
verified: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.onerror = function (error) {
|
||||
console.error('[Worker] Worker error:', error)
|
||||
self.postMessage({
|
||||
success: false,
|
||||
verified: false,
|
||||
error,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue