// 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('Local certificate test') }) 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() }