Implement managed browser security, individual agents and observability
This commit is contained in:
1 parent
ff482ebe23
commit
ea448de2d4
172 files changed
+5404
-347
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';import {tmpdir} from 'node:os';import {join} from 'node:path';import {execFileSync} from 'node:child_process';import {randomBytes,randomUUID} from 'node:crypto';
|
||||
import channel from '../packages/core/policy-channel.cjs';import browser from '../apps/browser/management-channel.cjs';import {openStore,digest} from '../apps/server/store.mjs';import {createApp} from '../apps/server/app.mjs';
|
||||
async function fixture(t){
|
||||
const dir=mkdtempSync(join(tmpdir(),'safetynet-pq-evidence-')),keyFile=join(dir,'evidence.key');writeFileSync(keyFile,randomBytes(32),{mode:0o600});if(process.platform==='win32')execFileSync(join(process.env.SystemRoot,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoProfile','-NonInteractive','-Command',"$ErrorActionPreference='Stop';$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User;$acl=New-Object System.Security.AccessControl.FileSecurity;$acl.SetOwner($sid);$acl.SetAccessRuleProtection($true,$false);$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl','Allow')));Set-Acl -LiteralPath $env:TEST_PQ_EVIDENCE_KEY -AclObject $acl"],{windowsHide:true,env:{SystemRoot:process.env.SystemRoot,TEST_PQ_EVIDENCE_KEY:keyFile},stdio:'pipe'});
|
||||
const store=openStore(dir),p=store.get('policy');p.evidence={enabled:true,captureDownloads:true};store.put('policy',p);const token='synthetic-pq-evidence-device-token-123456789',otherToken='synthetic-pq-evidence-other-token-123456789',id='device-a';store.db.prepare('INSERT INTO devices(id,token_hash,revoked) VALUES(?,?,0)').run(id,digest(token));store.db.prepare('INSERT INTO devices(id,token_hash,revoked) VALUES(?,?,0)').run('device-b',digest(otherToken));
|
||||
const config={publicUrl:'http://127.0.0.1',secure:false,evidenceKeyFile:keyFile,requirePQManagement:true},server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));t.after(async()=>{server.closeAllConnections();await new Promise(r=>server.close(r));store.db.close();assert.ok(dir.startsWith(join(tmpdir(),'safetynet-pq-evidence-')));rmSync(dir,{recursive:true,force:true});});
|
||||
const base=`http://127.0.0.1:${server.address().port}`,wire=[];const api=async(route,body,credential=token)=>{wire.push({route,body});const r=await fetch(base+route,{method:body?'POST':'GET',headers:{authorization:`Bearer ${credential}`,...(body?{'content-type':'application/json'}:{})},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};};
|
||||
const eventId=randomUUID();store.event(id,'clipboard.copy',{eventId,policyVersion:1});const payload={eventId,type:'interaction',payload:{pageUrl:'https://intranet.example/file',action:'copy',content:'CONFIDENTIAL_SYNTHETIC_PQ_EVIDENCE',sensitiveField:false}};
|
||||
const device={id,token,server:base,publicKey:store.publicKey(),channelSignatureKey:store.get('channelSigningKeys').publicKey};const make=async()=>{const challenge=await api('/api/device/channel/evidence/challenge',{});assert.equal(challenge.status,200);channel.evidenceChannel.verifyChallenge(challenge.body,id,device.publicKey,device.channelSignatureKey);return {bundle:challenge.body,context:channel.evidenceChannel.prepare(challenge.body,payload)};};return {store,api,wire,payload,device,make};
|
||||
}
|
||||
test('real central PQ evidence storage rejects plaintext and supports authenticated idempotent retry',async t=>{
|
||||
const f=await fixture(t);assert.equal((await f.api('/api/device/evidence',f.payload)).status,403);
|
||||
const request=async(_base,route,body,credential)=>{const response=await f.api(route,body,credential);if(response.status>=400)throw Object.assign(Error('Transport refused'),{status:response.status});return response.body;};
|
||||
const created=await browser.protectedEvidenceRequest({device:f.device,request,save:()=>{},payload:f.payload});assert.match(created.id,/^[0-9a-f-]{36}$/);const again=await browser.protectedEvidenceRequest({device:f.device,request,save:()=>{},payload:f.payload});assert.equal(again.id,created.id);assert.equal(again.replayed,true);
|
||||
assert.equal(f.store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,1);assert.equal(f.store.db.prepare("SELECT count(*) n FROM events WHERE kind='evidence.recorded'").get().n,1);
|
||||
assert.equal(JSON.stringify(f.wire.filter(r=>r.route.includes('/channel/'))).includes(f.payload.payload.content),false);assert.equal(f.store.db.prepare('SELECT metadata FROM events').all().some(r=>r.metadata.includes(f.payload.payload.content)),false);
|
||||
const row=f.store.db.prepare('SELECT ciphertext FROM protected_evidence').get();assert.equal(Buffer.from(row.ciphertext).includes(Buffer.from(f.payload.payload.content)),false);
|
||||
await assert.rejects(browser.protectedEvidenceRequest({device:f.device,request,save:()=>{},payload:{...f.payload,payload:{...f.payload.payload,content:'Different synthetic payload'}}}),e=>e.status===409);
|
||||
});
|
||||
test('challenge consumption covers successful replay, invalid ciphertext and cross-device substitution',async t=>{
|
||||
const f=await fixture(t);let a=await f.make();assert.equal((await f.api('/api/device/channel/evidence',a.context.request,'synthetic-pq-evidence-other-token-123456789')).status,403);
|
||||
let response=await f.api('/api/device/channel/evidence',a.context.request);assert.equal(response.status,200);assert.equal(channel.evidenceChannel.finish(response.body,a.bundle,a.context).status,201);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);a.context.responseKey.fill(0);
|
||||
a=await f.make();const altered=structuredClone(a.context.request);altered.encrypted.tag=Buffer.alloc(16).toString('base64url');assert.equal((await f.api('/api/device/channel/evidence',altered)).status,403);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);a.context.responseKey.fill(0);
|
||||
});
|
||||
test('central disable after issuance returns encrypted denial and revoked credentials cannot upload',async t=>{
|
||||
const f=await fixture(t),a=await f.make(),p=f.store.get('policy');p.evidence.enabled=false;f.store.put('policy',p);const response=await f.api('/api/device/channel/evidence',a.context.request);assert.equal(response.status,200);assert.equal(channel.evidenceChannel.finish(response.body,a.bundle,a.context).status,403);assert.equal(f.store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,0);assert.equal((await f.api('/api/device/channel/evidence/challenge',{})).status,403);a.context.responseKey.fill(0);
|
||||
p.evidence.enabled=true;f.store.put('policy',p);const b=await f.make();f.store.db.prepare('UPDATE devices SET revoked=1 WHERE id=?').run(f.device.id);assert.equal((await f.api('/api/device/channel/evidence',b.context.request)).status,401);b.context.responseKey.fill(0);
|
||||
});
|
||||
test('expired evidence challenge never decrypts or stores payload and remains one-use',async t=>{
|
||||
const now=Date.now();t.mock.timers.enable({apis:['Date'],now});const f=await fixture(t),a=await f.make();t.mock.timers.setTime(now+60001);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);assert.equal((await f.api('/api/device/channel/evidence',a.context.request)).status,403);assert.equal(f.store.db.prepare('SELECT count(*) n FROM protected_evidence').get().n,0);a.context.responseKey.fill(0);
|
||||
});
|
||||
Reference in new issue
Block a user