62 lines
6.9 KiB
JavaScript
62 lines
6.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {generateKeyPairSync} from 'node:crypto';
|
|
import {mkdtempSync,rmSync} from 'node:fs';
|
|
import {tmpdir} from 'node:os';
|
|
import {join} from 'node:path';
|
|
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';
|
|
import {verifyPolicy} from '../packages/core/policy.mjs';
|
|
const ed=generateKeyPairSync('ed25519'),pq=generateKeyPairSync('ml-dsa-87');
|
|
const edPublic=ed.publicKey.export({type:'spki',format:'pem'}),pqPublic=pq.publicKey.export({type:'spki',format:'pem'});
|
|
const make=()=>channel.challenge('device-a',ed.privateKey,pq.privateKey);
|
|
|
|
test('real ML-KEM-1024 and dual signatures protect policy bytes with separate AEAD directions',()=>{
|
|
const identity=channel.identity(ed.privateKey,pq.privateKey,pqPublic);assert.equal(channel.verifyIdentity(identity,edPublic,pqPublic),pqPublic);
|
|
const {bundle,privateKey}=make();channel.verifyChallenge(bundle,'device-a',edPublic,pqPublic);
|
|
const client=channel.prepare(bundle),server=channel.receive(client.request,bundle,privateKey);
|
|
const value={payload:'confidential synthetic policy content',signature:'synthetic signature'};
|
|
const response=channel.response(value,bundle,server);assert.equal(JSON.stringify(response).includes(value.payload),false);assert.deepEqual(channel.finish(response,bundle,client),value);
|
|
assert.throws(()=>channel.finish({...response,encrypted:client.request.encrypted},bundle,client));
|
|
const modified=structuredClone(response);modified.encrypted.tag=Buffer.alloc(16).toString('base64url');assert.throws(()=>channel.finish(modified,bundle,client));
|
|
client.responseKey.fill(0);server.responseKey.fill(0);
|
|
});
|
|
test('channel rejects signature substitutions, different devices, expiry and unrecognized algorithms',()=>{
|
|
const {bundle}=make();assert.throws(()=>channel.verifyChallenge(bundle,'device-b',edPublic,pqPublic));
|
|
assert.throws(()=>channel.verifyChallenge(bundle,'device-a',edPublic,pqPublic,bundle.expiresAt));
|
|
const other=generateKeyPairSync('ml-dsa-87').publicKey.export({type:'spki',format:'pem'});assert.throws(()=>channel.verifyChallenge(bundle,'device-a',edPublic,other));
|
|
assert.throws(()=>channel.verifyChallenge({...bundle,version:'downgraded'},'device-a',edPublic,pqPublic));
|
|
assert.throws(()=>channel.verifyChallenge({...bundle,extra:'untrusted'},'device-a',edPublic,pqPublic));
|
|
assert.throws(()=>channel.verifyIdentity(channel.identity(ed.privateKey,pq.privateKey,pqPublic),edPublic,other));
|
|
});
|
|
test('ciphertext changes and response splicing never release a policy',()=>{
|
|
const a=make(),b=make(),client=channel.prepare(a.bundle),other=channel.prepare(b.bundle),server=channel.receive(client.request,a.bundle,a.privateKey);
|
|
const altered=structuredClone(client.request);altered.encrypted.ciphertext=Buffer.from('invalid').toString('base64url');assert.throws(()=>channel.receive(altered,a.bundle,a.privateKey));
|
|
assert.throws(()=>channel.receive({...client.request,deviceId:'device-b'},a.bundle,a.privateKey));
|
|
assert.throws(()=>channel.finish(channel.response({payload:'safe'},a.bundle,server),b.bundle,other));
|
|
const replayContext={...client,requestId:'different-request'};assert.throws(()=>channel.finish(channel.response({payload:'safe'},a.bundle,server),a.bundle,replayContext));
|
|
client.responseKey.fill(0);other.responseKey.fill(0);server.responseKey.fill(0);
|
|
});
|
|
test('real central channel denies plaintext, replay and cross-device access; browser never falls back',async t=>{
|
|
const directory=mkdtempSync(join(tmpdir(),'safetynet-channel-')),store=openStore(directory),config={publicUrl:'http://127.0.0.1',secure:false,adminToken:'test-only-admin-credential-01234567890',requirePQManagement:true};
|
|
const server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));const base=`http://127.0.0.1:${server.address().port}`;
|
|
t.after(async()=>{server.closeAllConnections();await new Promise(resolve=>server.close(resolve));store.db.close();assert.ok(directory.startsWith(join(tmpdir(),'safetynet-channel-')));rmSync(directory,{recursive:true,force:true});});
|
|
async function api(route,body,credential){const response=await fetch(base+route,{method:body?'POST':'GET',headers:{...(body?{'content-type':'application/json'}:{}),...(credential?{authorization:`Bearer ${credential}`}:{})},body:body?JSON.stringify(body):undefined});return {status:response.status,value:await response.json()};}
|
|
async function enroll(name){const code='synthetic-enrollment-code-'+name;store.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(digest(code),Date.now()+60000,name);const result=await api('/api/device/enroll',{code,name,platform:'win32'});assert.equal(result.status,200);return result.value;}
|
|
const a=await enroll('a'),b=await enroll('b');assert.equal(Object.hasOwn(a,'policy'),false,'Mandatory protected bootstrap must not disclose a plaintext policy');assert.equal((await api('/api/device/policy',null,a.token)).status,403);
|
|
assert.equal((await api('/api/admin/management-channel',null,a.token)).status,401);
|
|
const publicChannel=await api('/api/admin/management-channel',null,config.adminToken);assert.equal(publicChannel.status,200);assert.deepEqual(Object.keys(publicChannel.value).sort(),['requirePQManagement','signatureKey','version']);assert.equal(publicChannel.value.signatureKey,a.managementChannel.signatureKey);assert.equal(JSON.stringify(publicChannel.value).includes('PRIVATE KEY'),false);
|
|
const bundle=(await api('/api/device/channel/challenge',{},a.token)).value,context=channel.prepare(bundle);
|
|
assert.equal((await api('/api/device/channel/policy',context.request,b.token)).status,403);
|
|
const protectedResponse=await api('/api/device/channel/policy',context.request,a.token);assert.equal(protectedResponse.status,200);
|
|
assert.equal(verifyPolicy(channel.finish(protectedResponse.value,bundle,context),a.publicKey).version,1);context.responseKey.fill(0);
|
|
assert.equal((await api('/api/device/channel/policy',context.request,a.token)).status,403);
|
|
const device={server:base,id:a.id,token:a.token,publicKey:a.publicKey,channelSignatureKey:channel.verifyIdentity(a.managementChannel,a.publicKey)};const called=[];
|
|
const request=async(_server,route,body,token)=>{called.push(route);const result=await api(route,body,token);if(result.status!==200)throw Error('Refused');return result.value;};
|
|
const policy=await browser.protectedPolicyRequest({device,request,save:()=>assert.fail('Fresh enrollment must retain its signing pin')});assert.equal(verifyPolicy(policy,a.publicKey).version,1);assert.equal(called.includes('/api/device/policy'),false);
|
|
await assert.rejects(browser.protectedPolicyRequest({device,request:async()=>{throw Error('Transport offline');},save:()=>{}}));
|
|
store.db.prepare('UPDATE devices SET revoked=1 WHERE id=?').run(a.id);assert.equal((await api('/api/device/channel/challenge',{},a.token)).status,401);
|
|
});
|