Skip to content

Commit a530f77

Browse files
authored
fix: tolerate unsigned validate manifests
1 parent 2be9d3a commit a530f77

8 files changed

Lines changed: 195 additions & 12 deletions

File tree

.aiox-core/cli/commands/validate/index.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* aiox validate --repair # Repair missing/corrupted files
1414
* aiox validate --repair --dry-run # Preview repairs without applying
1515
* aiox validate --detailed # Show detailed file list
16+
* aiox validate --require-signature # Enforce signed manifests
1617
* aiox validate --json # Output as JSON
1718
*/
1819

@@ -63,6 +64,10 @@ function createValidateCommand() {
6364
.option('--detailed', 'Show detailed file list')
6465
.option('--no-hash', 'Skip hash verification (faster)')
6566
.option('--no-signature', 'Skip manifest signature verification (insecure; recovery only)')
67+
.option(
68+
'--require-signature',
69+
'Fail when the manifest signature is missing (also enabled by AIOX_REQUIRE_SIGNATURE=1)',
70+
)
6671
.option('--extras', 'Detect extra files not in manifest')
6772
.option('-v, --verbose', 'Enable verbose output')
6873
.option('--json', 'Output results as JSON')
@@ -86,6 +91,9 @@ Examples:
8691
${chalk.dim('# Quick validation (skip hash check)')}
8792
$ aiox validate --no-hash
8893
94+
${chalk.dim('# Strict validation (requires .minisig)')}
95+
$ aiox validate --require-signature
96+
8997
${chalk.dim('# Output as JSON for CI/CD')}
9098
$ aiox validate --json
9199
@@ -189,7 +197,7 @@ async function runValidation(options) {
189197
// Try to find source in common locations
190198
const possibleSources = [
191199
path.join(__dirname, '../../../../..'), // npm package root
192-
path.join(projectRoot, 'node_modules/aiox-core'),
200+
path.join(projectRoot, 'node_modules/@aiox-squads/core'),
193201
path.join(projectRoot, 'node_modules/aiox-core'),
194202
];
195203

@@ -201,6 +209,12 @@ async function runValidation(options) {
201209
}
202210
}
203211

212+
const requireSignature = resolveSignatureRequirement({
213+
options,
214+
aioxCoreDir,
215+
sourceDir,
216+
});
217+
204218
// Show spinner for non-JSON output (must be defined before validator for closure)
205219
let spinner = null;
206220
if (!options.json) {
@@ -214,7 +228,7 @@ async function runValidation(options) {
214228
// Create validator instance
215229
const validator = new PostInstallValidator(projectRoot, sourceDir, {
216230
verifyHashes: options.hash !== false,
217-
requireSignature: options.signature !== false,
231+
requireSignature,
218232
detectExtras: options.extras === true,
219233
verbose: options.verbose === true,
220234
onProgress: options.json
@@ -266,6 +280,7 @@ async function runValidation(options) {
266280
status: report.status,
267281
integrityScore: report.integrityScore,
268282
manifestVerified: report.manifestVerified,
283+
signatureRequired: report.signatureRequired,
269284
timestamp: report.timestamp,
270285
duration: report.duration,
271286
manifest: report.manifest,
@@ -343,6 +358,54 @@ async function runValidation(options) {
343358
}
344359
}
345360

361+
/**
362+
* Resolve whether validate should require manifest signature verification.
363+
*
364+
* Default behavior verifies signatures automatically when a .minisig is
365+
* present, but it does not hard-fail public packages that were distributed
366+
* without the signature artifact. Strict mode remains available via
367+
* --require-signature or AIOX_REQUIRE_SIGNATURE=1.
368+
*
369+
* @param {Object} params - Resolution params
370+
* @param {Object} params.options - Commander options
371+
* @param {string} params.aioxCoreDir - Installed .aiox-core directory
372+
* @param {string|null} [params.sourceDir] - Source package directory
373+
* @param {Object} [params.env] - Environment object for tests
374+
* @returns {boolean} True when validation must require a valid signature
375+
*/
376+
function resolveSignatureRequirement({ options = {}, aioxCoreDir, sourceDir = null, env = process.env }) {
377+
if (options.signature === false) {
378+
return false;
379+
}
380+
381+
if (options.requireSignature === true || isTruthyEnv(env.AIOX_REQUIRE_SIGNATURE)) {
382+
return true;
383+
}
384+
385+
const targetSignaturePath = path.join(aioxCoreDir, 'install-manifest.yaml.minisig');
386+
if (fs.existsSync(targetSignaturePath)) {
387+
return true;
388+
}
389+
390+
if (sourceDir) {
391+
const sourceSignaturePath = path.join(sourceDir, '.aiox-core', 'install-manifest.yaml.minisig');
392+
if (fs.existsSync(sourceSignaturePath)) {
393+
return true;
394+
}
395+
}
396+
397+
return false;
398+
}
399+
400+
/**
401+
* Interpret common truthy environment variable values.
402+
* @param {string|undefined} value - Raw environment value
403+
* @returns {boolean} True when the value enables the setting
404+
*/
405+
function isTruthyEnv(value) {
406+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
407+
}
408+
346409
/**
347410
* Run repair process
348411
* @param {PostInstallValidator} validator - Validator instance
@@ -428,4 +491,5 @@ function truncatePath(filePath, maxLen) {
428491

429492
module.exports = {
430493
createValidateCommand,
494+
resolveSignatureRequirement,
431495
};

.aiox-core/install-manifest.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# - SHA256 hashes for change detection
88
# - File types for categorization
99
#
10-
version: 5.1.0
11-
generated_at: "2026-05-07T08:02:17.787Z"
10+
version: 5.1.1
11+
generated_at: "2026-05-07T10:20:29.036Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1103
1414
files:
@@ -121,9 +121,9 @@ files:
121121
type: cli
122122
size: 5320
123123
- path: cli/commands/validate/index.js
124-
hash: sha256:dcf19f1b4a46e562812c5c718783e88f9ac8ef0bf51f27eb1a6d31c7081fafdf
124+
hash: sha256:8c695b2cde5390a867922f37dd5a89c539fe4c688303a1eb52f097a747d599ad
125125
type: cli
126-
size: 13040
126+
size: 15206
127127
- path: cli/commands/workers/formatters/info-formatter.js
128128
hash: sha256:11c17e16be0b7d09ba8949497e0887bb20996966d266454aa2bb5dfc9d9d91b8
129129
type: cli

docs/security/MANIFEST_SIGNING.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,17 @@ The signature blob encoding follows the minisign specification. For reference, t
186186

187187
**Note**: Applications should use the verification module (`manifest-signature.js`) rather than parsing the signature format directly. The exact wire format is defined by the minisign specification and may vary in optional fields.
188188

189+
## CLI Verification Modes
190+
191+
The `aiox validate` command uses a compatibility-safe default:
192+
193+
- If `.aiox-core/install-manifest.yaml.minisig` exists in the target or repair source, the signature is verified automatically.
194+
- If no `.minisig` was distributed with the package, validation continues without signature verification and still verifies file hashes.
195+
- Strict enforcement is available with `aiox validate --require-signature` or `AIOX_REQUIRE_SIGNATURE=1`.
196+
- Emergency recovery can explicitly bypass verification with `aiox validate --no-signature`.
197+
198+
This keeps unsigned public packages installable while preserving an explicit strict mode for signed releases and controlled environments.
199+
189200
## Development Mode
190201

191202
During development and testing, signature verification can be bypassed:
@@ -203,14 +214,16 @@ const validator = new PostInstallValidator(projectRoot, frameworkRoot, {
203214
- Tampered manifests will be accepted
204215
- The trust chain is broken
205216

206-
This option exists **exclusively** for local development environments. Production builds **MUST** enforce signature verification (`requireSignature: true`). Any deployment with signature verification disabled should be considered insecure.
217+
This option exists **exclusively** for local development, recovery, and unsigned package compatibility. Production builds that distribute `.minisig` artifacts **MUST** enforce signature verification (`requireSignature: true`) or run `aiox validate --require-signature`. Any deployment with signature verification disabled should be considered unsigned.
207218

208219
## Verification Behavior
209220

210221
| Mode | Signature Missing | Invalid Signature | Valid Signature |
211222
| --------------------------------------- | ----------------- | ----------------- | --------------- |
212223
| Production (`requireSignature: true`) | ERROR | ERROR | OK |
213224
| Development (`requireSignature: false`) | WARN | ERROR | OK |
225+
| CLI default, no `.minisig` distributed | WARN | ERROR | OK |
226+
| CLI default, `.minisig` present | ERROR | ERROR | OK |
214227

215228
## Troubleshooting
216229

@@ -281,7 +294,7 @@ Loads and verifies a manifest file.
281294
**Parameters:**
282295

283296
- `manifestPath` (string): Path to manifest file
284-
- `options.requireSignature` (boolean): Fail if signature missing (default: true)
297+
- `options.requireSignature` (boolean): Fail if signature missing (default: true for the library API; CLI default is resolved by `.minisig` presence or strict flags)
285298

286299
**Returns:**
287300

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aiox-squads/core",
3-
"version": "5.1.0",
3+
"version": "5.1.1",
44
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
55
"bin": {
66
"aiox": "bin/aiox.js",

packages/installer/src/installer/post-install-validator.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,7 @@ class PostInstallValidator {
10951095
status,
10961096
integrityScore,
10971097
manifestVerified: this.manifestVerified,
1098+
signatureRequired: this.options.requireSignature,
10981099
timestamp: new Date().toISOString(),
10991100
duration: `${duration}ms`,
11001101
manifest: this.manifest
@@ -1419,8 +1420,10 @@ function formatReport(report, options = {}) {
14191420
// Signature status
14201421
if (report.manifestVerified) {
14211422
lines.push(`${c.green}${c.reset} Manifest signature: ${c.green}VERIFIED${c.reset}`);
1422-
} else {
1423+
} else if (report.signatureRequired) {
14231424
lines.push(`${c.red}${c.reset} Manifest signature: ${c.red}NOT VERIFIED${c.reset}`);
1425+
} else {
1426+
lines.push(`${c.yellow}${c.reset} Manifest signature: ${c.yellow}NOT REQUIRED${c.reset}`);
14241427
}
14251428

14261429
// Status
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
const fs = require('fs-extra');
4+
const os = require('os');
5+
const path = require('path');
6+
7+
const {
8+
resolveSignatureRequirement,
9+
} = require('../../.aiox-core/cli/commands/validate/index');
10+
11+
describe('validate signature mode', () => {
12+
let testDir;
13+
let aioxCoreDir;
14+
let sourceDir;
15+
16+
beforeEach(async () => {
17+
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'aiox-validate-signature-'));
18+
aioxCoreDir = path.join(testDir, 'project', '.aiox-core');
19+
sourceDir = path.join(testDir, 'source');
20+
await fs.ensureDir(aioxCoreDir);
21+
await fs.ensureDir(path.join(sourceDir, '.aiox-core'));
22+
});
23+
24+
afterEach(async () => {
25+
await fs.remove(testDir);
26+
});
27+
28+
test('does not require signature when public package lacks minisig', () => {
29+
expect(
30+
resolveSignatureRequirement({
31+
options: {},
32+
aioxCoreDir,
33+
sourceDir,
34+
env: {},
35+
}),
36+
).toBe(false);
37+
});
38+
39+
test('requires signature when strict flag is set', () => {
40+
expect(
41+
resolveSignatureRequirement({
42+
options: { requireSignature: true },
43+
aioxCoreDir,
44+
sourceDir,
45+
env: {},
46+
}),
47+
).toBe(true);
48+
});
49+
50+
test('requires signature when strict env is set', () => {
51+
expect(
52+
resolveSignatureRequirement({
53+
options: {},
54+
aioxCoreDir,
55+
sourceDir,
56+
env: { AIOX_REQUIRE_SIGNATURE: 'true' },
57+
}),
58+
).toBe(true);
59+
});
60+
61+
test('no-signature flag overrides strict env and local signature', async () => {
62+
await fs.writeFile(path.join(aioxCoreDir, 'install-manifest.yaml.minisig'), 'signature');
63+
64+
expect(
65+
resolveSignatureRequirement({
66+
options: { signature: false },
67+
aioxCoreDir,
68+
sourceDir,
69+
env: { AIOX_REQUIRE_SIGNATURE: '1' },
70+
}),
71+
).toBe(false);
72+
});
73+
74+
test('requires signature automatically when target minisig exists', async () => {
75+
await fs.writeFile(path.join(aioxCoreDir, 'install-manifest.yaml.minisig'), 'signature');
76+
77+
expect(
78+
resolveSignatureRequirement({
79+
options: {},
80+
aioxCoreDir,
81+
sourceDir,
82+
env: {},
83+
}),
84+
).toBe(true);
85+
});
86+
87+
test('requires signature automatically when source minisig exists', async () => {
88+
await fs.writeFile(
89+
path.join(sourceDir, '.aiox-core', 'install-manifest.yaml.minisig'),
90+
'signature',
91+
);
92+
93+
expect(
94+
resolveSignatureRequirement({
95+
options: {},
96+
aioxCoreDir,
97+
sourceDir,
98+
env: {},
99+
}),
100+
).toBe(true);
101+
});
102+
});

tests/installer/post-install-validator.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ describe('PostInstallValidator Security Tests', () => {
243243

244244
// Should succeed without signature in dev mode
245245
expect(report.manifestVerified).toBe(false);
246+
expect(report.signatureRequired).toBe(false);
246247
expect(report.status).not.toBe('failed');
247248
});
248249
});

0 commit comments

Comments
 (0)