120 lines
7.3 KiB
JavaScript
120 lines
7.3 KiB
JavaScript
// Real identity-module + native DTLS test; full app pairing is tested separately.
|
|
// Synthetic local peers, isolated browser context, no account or Google requests.
|
|
import { chromium } from 'playwright'
|
|
import { createServer } from 'node:http'
|
|
import { readFile } from 'node:fs/promises'
|
|
import assert from 'node:assert/strict'
|
|
|
|
const server = createServer(async (request, response) => {
|
|
if (request.url === '/peer-identity.mjs') {
|
|
try {
|
|
const source = await readFile(new URL('../src/peer-identity.mjs', import.meta.url))
|
|
response.writeHead(200, { 'Content-Type': 'text/javascript' }); response.end(source)
|
|
} catch { response.writeHead(500); response.end() }
|
|
return
|
|
}
|
|
response.writeHead(200, { 'Content-Type': 'text/html' })
|
|
response.end('<!doctype html><title>Local certificate test</title>')
|
|
})
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
|
let browser
|
|
try {
|
|
browser = await chromium.launch({ channel: 'msedge', headless: true })
|
|
const page = await browser.newPage()
|
|
page.setDefaultTimeout(20000)
|
|
await page.goto(`http://127.0.0.1:${server.address().port}`)
|
|
const original = await page.evaluate(async () => {
|
|
const { createDeviceIdentity } = await import('/peer-identity.mjs')
|
|
const results = await Promise.all(Array.from({ length: 4 }, () => createDeviceIdentity('11111111-1111-4111-8111-111111111111')))
|
|
if (new Set(results.map(value => value.fingerprint)).size !== 1) throw new Error('Concurrent identity creation diverged')
|
|
const { certificate, fingerprint } = results[0]
|
|
return { fingerprints: certificate.getFingerprints(), fingerprint, expires: certificate.expires }
|
|
})
|
|
await page.reload()
|
|
const result = await page.evaluate(async expected => {
|
|
const { loadDeviceIdentity, assertRemoteIdentity } = await import('/peer-identity.mjs')
|
|
const { certificate } = await loadDeviceIdentity('11111111-1111-4111-8111-111111111111', expected)
|
|
let missingRejected = false, wrongPinRejected = false
|
|
try { await loadDeviceIdentity('22222222-2222-4222-8222-222222222222', expected) } catch { missingRejected = true }
|
|
try { await loadDeviceIdentity('11111111-1111-4111-8111-111111111111', Array(32).fill('00').join(':')) } catch { wrongPinRejected = true }
|
|
const peers = []
|
|
const complete = async pc => {
|
|
if (pc.iceGatheringState === 'complete') return
|
|
await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => { cleanup(); reject(new Error('ICE timeout')) }, 10000)
|
|
const check = () => { if (pc.iceGatheringState === 'complete') { cleanup(); resolve() } }
|
|
const cleanup = () => { clearTimeout(timeout); pc.removeEventListener('icegatheringstatechange', check) }
|
|
pc.addEventListener('icegatheringstatechange', check); check()
|
|
})
|
|
}
|
|
try {
|
|
const phone = new RTCPeerConnection({ iceServers: [] })
|
|
const desktop = new RTCPeerConnection({ iceServers: [], certificates: [certificate] })
|
|
peers.push(phone, desktop)
|
|
const channel = phone.createDataChannel('certificate-probe')
|
|
desktop.ondatachannel = event => { event.channel.onmessage = message => event.channel.send(message.data) }
|
|
await phone.setLocalDescription(await phone.createOffer()); await complete(phone)
|
|
await desktop.setRemoteDescription(phone.localDescription)
|
|
await desktop.setLocalDescription(await desktop.createAnswer()); await complete(desktop)
|
|
const advertised = desktop.localDescription.sdp.split(/\r?\n/).filter(line => line.startsWith('a=fingerprint:'))
|
|
assertRemoteIdentity(desktop.localDescription.sdp, expected)
|
|
await phone.setRemoteDescription(desktop.localDescription)
|
|
const echo = await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => reject(new Error('DTLS echo timeout')), 15000)
|
|
channel.onmessage = event => { clearTimeout(timeout); resolve(event.data) }
|
|
channel.onopen = () => channel.send('Türkçe kimlik doğrulama 🟢')
|
|
if (channel.readyState === 'open') channel.onopen()
|
|
})
|
|
const goodDtls = phone.sctp.transport.state
|
|
const wrongCertificate = await RTCPeerConnection.generateCertificate({ name: 'ECDSA', namedCurve: 'P-256' })
|
|
const wrongFingerprint = wrongCertificate.getFingerprints().find(item => item.algorithm === 'sha-256').value.toUpperCase()
|
|
let untrustedPeerRejected = false
|
|
try { assertRemoteIdentity(desktop.localDescription.sdp, wrongFingerprint) } catch { untrustedPeerRejected = true }
|
|
const badPhone = new RTCPeerConnection({ iceServers: [] })
|
|
const badDesktop = new RTCPeerConnection({ iceServers: [], certificates: [certificate] })
|
|
peers.push(badPhone, badDesktop)
|
|
const badChannel = badPhone.createDataChannel('certificate-mismatch')
|
|
let mismatchOpened = false
|
|
badChannel.onopen = () => { mismatchOpened = true }
|
|
await badPhone.setLocalDescription(await badPhone.createOffer()); await complete(badPhone)
|
|
await badDesktop.setRemoteDescription(badPhone.localDescription)
|
|
await badDesktop.setLocalDescription(await badDesktop.createAnswer()); await complete(badDesktop)
|
|
await badPhone.setRemoteDescription({ type: 'answer', sdp: badDesktop.localDescription.sdp
|
|
.split(/\r?\n/).map(line => line.startsWith('a=fingerprint:sha-256 ')
|
|
? `a=fingerprint:sha-256 ${wrongFingerprint}` : line).join('\r\n') })
|
|
const mismatchState = await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => { cleanup(); reject(new Error('No definitive certificate rejection')) }, 15000)
|
|
const check = () => {
|
|
const state = badPhone.sctp?.transport.state
|
|
if (state === 'failed' || state === 'connected') { cleanup(); resolve(state) }
|
|
}
|
|
const cleanup = () => { clearTimeout(timeout); badPhone.removeEventListener('connectionstatechange', check) }
|
|
badPhone.addEventListener('connectionstatechange', check); check()
|
|
})
|
|
return { fingerprints: certificate.getFingerprints(), expires: certificate.expires,
|
|
nativeCertificate: certificate instanceof RTCCertificate, advertised, echo,
|
|
dtls: goodDtls, mismatchState, mismatchOpened, missingRejected, wrongPinRejected, untrustedPeerRejected }
|
|
} finally { for (const peer of peers) peer.close() }
|
|
}, original.fingerprint)
|
|
assert.equal(result.nativeCertificate, true)
|
|
assert.deepEqual(result.fingerprints, original.fingerprints)
|
|
assert.equal(result.expires, original.expires)
|
|
assert.ok(result.expires > Date.now())
|
|
const sha256 = original.fingerprints.find(item => item.algorithm === 'sha-256')
|
|
assert.ok(sha256)
|
|
assert.ok(result.advertised.some(line => line.toLowerCase() === `a=fingerprint:sha-256 ${sha256.value}`.toLowerCase()))
|
|
assert.equal(result.echo, 'Türkçe kimlik doğrulama 🟢')
|
|
assert.equal(result.dtls, 'connected')
|
|
assert.equal(result.mismatchState, 'failed')
|
|
assert.equal(result.mismatchOpened, false)
|
|
assert.equal(result.missingRejected, true)
|
|
assert.equal(result.wrongPinRejected, true)
|
|
assert.equal(result.untrustedPeerRejected, true)
|
|
console.log(JSON.stringify({ nativeCertificatePersistence: 'passed', reload: 'passed',
|
|
restoredCertificateDtlsEcho: 'passed', nativeFingerprintMismatch: 'rejected',
|
|
concurrentCreation: 'passed', missingKeyAndWrongPin: 'rejected', scope: 'identity module and native DTLS; not full app pairing' }))
|
|
} finally {
|
|
await browser?.close()
|
|
server.close()
|
|
}
|