Require signed agent releases (Refs #64)

This commit is contained in:
2026-10-09 20:36:18 -04:00
parent df0d395138
commit 6f71822d41
24 changed files with 984 additions and 107 deletions
+2 -1
View File
@@ -499,7 +499,8 @@ describe('machine and route identity boundaries', () => {
'/agent/ec-agent.py',
'/agent/ec-agent-update.py',
'/agent/install.sh',
'/agent/manifest.json'
'/agent/manifest.json',
'/agent/manifest.json.sig'
]) {
const call = requestFor(null, { path });
expect((await call.invoke()).status).toBe(200);
+27 -8
View File
@@ -24,7 +24,8 @@ describe('public agent release artifacts', () => {
'install.sh',
'ec-agent.py',
'ec-agent-update.py',
'manifest.json'
'manifest.json',
'manifest.json.sig'
] as const) {
expect(await agentReleaseAsset(file)).toBe('fresh-release');
}
@@ -32,7 +33,8 @@ describe('public agent release artifacts', () => {
resolve('static/agent/install.sh'),
resolve('static/agent/ec-agent.py'),
resolve('static/agent/ec-agent-update.py'),
resolve('static/agent/manifest.json')
resolve('static/agent/manifest.json'),
resolve('static/agent/manifest.json.sig')
]);
});
@@ -54,7 +56,8 @@ describe('public agent release artifacts', () => {
'install.sh',
'ec-agent.py',
'ec-agent-update.py',
'manifest.json'
'manifest.json',
'manifest.json.sig'
] as const) {
expect(await agentReleaseAsset(file)).toBe('packaged-release');
}
@@ -62,23 +65,30 @@ describe('public agent release artifacts', () => {
resolve('build/client/agent/install.sh'),
resolve('build/client/agent/ec-agent.py'),
resolve('build/client/agent/ec-agent-update.py'),
resolve('build/client/agent/manifest.json')
resolve('build/client/agent/manifest.json'),
resolve('build/client/agent/manifest.json.sig')
]);
});
it.each(['ec-agent.py', 'ec-agent-update.py', 'manifest.json'])(
it.each(['ec-agent.py', 'ec-agent-update.py', 'manifest.json', 'manifest.json.sig'])(
'serves the packaged %s without a browser session or host credential',
async (file) => {
const content = file.endsWith('.json')
? '{"version":"1.2.0","updaterProtocolVersion":1}'
: '# verified release source\n';
? '{"version":"1.2.0","updaterProtocolVersion":2}'
: file.endsWith('.sig')
? '-----BEGIN PGP SIGNATURE-----\nrelease-signature\n-----END PGP SIGNATURE-----\n'
: '# verified release source\n';
files.readFile.mockResolvedValue(content);
const response = await GET({ params: { file } });
expect(response.status).toBe(200);
expect(await response.text()).toBe(content);
expect(response.headers.get('cache-control')).toBe('no-cache');
expect(response.headers.get('content-type')).toContain(
file.endsWith('.json') ? 'application/json' : 'text/plain'
file.endsWith('.sig')
? 'application/pgp-signature'
: file.endsWith('.json')
? 'application/json'
: 'text/plain'
);
expect(files.readFile.mock.calls).toEqual([
[resolve('build/client/agent', file), 'utf8']
@@ -86,6 +96,13 @@ describe('public agent release artifacts', () => {
}
);
it('returns 404 for an unsigned release without generating a signature', async () => {
files.readFile.mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' }));
await expect(GET({ params: { file: 'manifest.json.sig' } })).rejects.toMatchObject({
status: 404
});
});
it('serves the updater from development assets when no packaged asset exists', async () => {
files.readFile
.mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' }))
@@ -111,6 +128,8 @@ describe('public agent release artifacts', () => {
'update-status.json',
'config.json',
'install.sh',
'manifest.json.sig/anything',
'release-signing.key',
'ec-agent-update.py/anything'
])('rejects non-release file %s before reading disk', async (file) => {
await expect(GET({ params: { file } })).rejects.toMatchObject({ status: 404 });
+7
View File
@@ -38,6 +38,13 @@ async function enrolled() {
}
describe('native host enrollment and scoped authentication', () => {
it('enrolls with an independently obtained local installer and quotes the token', async () => {
const { installerCommand } = await import('../../src/lib/server/hosts/http.js');
expect(installerCommand('https://controller.example/hosts/new', "token'quote")).toBe(
"sudo bash ./electric-cherry-install.sh --controller 'https://controller.example' --token 'token'\\''quote'"
);
});
it.each(['instance.inspect', 'rcon.status-batch'])(
'expires undelivered %s on timeout and delivers the next fresh check',
async (kind) => {