Add SSH host identity rotation controls
This commit is contained in:
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
|
||||
module.exports = async ({test, browser, adminBrowser, tick, json, session, failure, deferred, html}) => {
|
||||
const path='/api/settings/ssh', op=path+'-operation';
|
||||
const row=(id=9, extra={})=>({id,state:2,route:1,name_hex:'3c696d673e',closing:false,...extra});
|
||||
const fixture=(extra={})=>({generation:7,running:true,transitioning:false,sessions:[row(),row(10,{route:2})],...extra});
|
||||
const fixture=(extra={})=>({generation:7,running:true,transitioning:false,identity_generation:3,algorithm:'ecdsa-sha2-nistp256',fingerprint:'SHA256:'+'A'.repeat(43),rotatable:!extra.transitioning&&extra.generation!==4294967295,sessions:[row(),row(10,{route:2})],...extra});
|
||||
const reply=(state='pending',id=42,status=200,action='disconnect')=>new Response(JSON.stringify({id,action,state}),{status});
|
||||
const n=(b,id)=>b.nodes['ssh-'+id], posts=b=>b.calls.filter(c=>c.url===op&&c.method==='POST');
|
||||
async function open(v=fixture()) {const b=await adminBrowser();b.click('select-settings');await tick();b.queues[path].push(json(v));b.click('settings-ssh');await tick();return b;}
|
||||
@@ -13,7 +13,7 @@ module.exports = async ({test, browser, adminBrowser, tick, json, session, failu
|
||||
await test('SSH admin-only view, safe rows, no navigation/selection mutation and both terminal isolation',async()=>{
|
||||
const u=browser();u.start();await tick();u.click('settings-ssh');await tick();assert.equal(u.calls.filter(c=>c.url===path).length,0);
|
||||
const b=await open();assert.equal(b.nodes['ssh-settings'].hidden,false);
|
||||
assert.equal(n(b,'values').children[0].textContent,'9 / Serial / <img>');select(b);assert.equal(posts(b).length,0);
|
||||
assert.equal(n(b,'values').children[8].textContent,'9 / Serial / <img>');select(b);assert.equal(posts(b).length,0);
|
||||
for(let i=0;i<2;++i){b.sockets[i].emit('message',{data:Uint8Array.of(0,255,i).buffer});assert.deepEqual(b.terminals[i].writes.at(-1),[0,255,i]);b.terminals[i].input('blocked');assert.equal(b.sockets[i].sent.length,0);}
|
||||
assert.match(html,/Stop closes all SSH sessions/);assert.match(html,/HTTPS login, browser terminals, Wi-Fi, USB and UART0 are not stopped/);
|
||||
b.click('settings-broker');await tick();assert.equal(b.nodes['ssh-settings'].hidden,true);assert.equal(posts(b).length,0);
|
||||
@@ -29,6 +29,20 @@ module.exports = async ({test, browser, adminBrowser, tick, json, session, failu
|
||||
await refresh(b);assert.equal(posts(b).length,1);assert.equal(n(b,'target').value,'');
|
||||
}
|
||||
});
|
||||
await test('SSH host rotation confirms fingerprint and both generations, preserves HTTPS and never replays',async()=>{
|
||||
const b=await open(); let text=''; b.window.confirm=t=>{text=t;return false;}; b.click('ssh-rotate');await tick();assert.equal(posts(b).length,0);
|
||||
for(const pattern of [/SHA256:/,/identity generation 3/,/service generation 7/,/ALL SSH sessions/,/known_hosts/,/trusted UART0/,/ssh host-key info/,/HTTPS stays accessible/,/persistence fails/])assert.match(text,pattern);
|
||||
await submit(b,'rotate');assert.deepEqual(JSON.parse(posts(b)[0].body),{action:'rotate',generation:7,target:0,identity_generation:3});
|
||||
b.click('ssh-rotate');await tick();assert.equal(posts(b).length,1);assert.equal(b.sockets.length,2);
|
||||
b.queues[op].push(reply('failed',42,200,'rotate'));b.click('ssh-result');await tick();assert.match(n(b,'operation-detail').textContent,/persisted even if restart failed/);
|
||||
await refresh(b,fixture({identity_generation:4}));assert.equal(posts(b).length,1);
|
||||
});
|
||||
await test('SSH host metadata malformed or unavailable fails closed for rotation only',async()=>{
|
||||
for(const extra of [{identity_generation:0,fingerprint:''},{identity_generation:4294967295},{fingerprint:'private-key'},{algorithm:'ssh-rsa'}]){
|
||||
const b=await open(fixture(extra));assert.ok(n(b,'rotate').disabled);b.click('ssh-rotate');await tick();assert.equal(posts(b).length,0);
|
||||
}
|
||||
const b=await open(fixture({identity_generation:0,fingerprint:'',rotatable:false}));assert.ok(n(b,'rotate').disabled);assert.equal(n(b,'stop').disabled,false);
|
||||
});
|
||||
await test('SSH refresh never rebases explicit identity or resurrects stale and absent selections',async()=>{
|
||||
for(const v of [fixture({generation:8}),fixture({sessions:[row(13)]}),fixture({sessions:[row(9,{closing:true})]}),fixture({sessions:[row(9,{name_hex:'61'})]}),fixture({transitioning:true})]){
|
||||
const b=await open();select(b);await refresh(b,v);assert.ok(n(b,'disconnect').disabled);await refresh(b);assert.ok(n(b,'disconnect').disabled);assert.equal(n(b,'target').value,'');
|
||||
|
||||
Reference in New Issue
Block a user