Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions node_modules
33 changes: 33 additions & 0 deletions packages/create-agent-harness/__tests__/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,39 @@ describe('publishHarness (dry-run path)', () => {
});
});

// GH #4 (HIGH-1): a present witness whose signature was NOT cryptographically verified (degraded mode —
// no kernel verifier) must NOT publish silently as "signed". Before the fix the shape-only path returned
// valid:true, so a shape-valid witness carrying a garbage signature published as verified.
describe('publishHarness — fail-closed on an unverified witness (GH #4 HIGH-1)', () => {
async function harnessWithGarbageWitness(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'cah-pub-wit-'));
await mkdir(join(root, '.harness'), { recursive: true });
await writeFile(join(root, '.harness', 'manifest.json'),
JSON.stringify({ schema: 1, template: 'minimal', vars: { name: 'demo' } }));
// Shape-valid witness with a GARBAGE signature — the exploit input.
await writeFile(join(root, '.harness', 'witness.json'), JSON.stringify({
schema: 1, harness: 'demo', version: '0.1.0', entries: [],
public_key: 'a'.repeat(64), signature: 'b'.repeat(128),
}));
return root;
}

it('refuses to publish a harness whose witness signature was never checked', async () => {
const root = await harnessWithGarbageWitness();
await expect(publishHarness({
harnessDir: root, pinata: { jwt: 'x' }, confirm: false,
})).rejects.toThrow(/NOT cryptographically verified|--allow-unverified-witness/);
});

it('publishes when --allow-unverified-witness is explicitly opted in', async () => {
const root = await harnessWithGarbageWitness();
const r = await publishHarness({
harnessDir: root, pinata: { jwt: 'x' }, confirm: false, allowUnverified: true,
});
expect(r.manifestCid).toBe('dry-run-no-pin');
});
});

describe('pinJson', () => {
it('throws on missing JWT', async () => {
await expect(pinJson({ jwt: '' }, { foo: 'bar' }, { name: 't' }))
Expand Down
5 changes: 5 additions & 0 deletions packages/create-agent-harness/src/publish-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ export async function publishCmd(args: string[]): Promise<SubcommandResult> {
const positional = args.filter(a => !a.startsWith('--'));
const dir = resolve(positional[0] ?? process.cwd());
const confirm = args.includes('--confirm');
const allowUnverified = args.includes('--allow-unverified-witness');
const nameOverride = args.find(a => a.startsWith('--name='))?.slice('--name='.length);

const lines: string[] = [`harness publish — ${dir} ${confirm ? '(CONFIRMED)' : '(DRY-RUN)'}`];
if (allowUnverified) {
lines.push(' ⚠️ --allow-unverified-witness: the witness signature will NOT be cryptographically checked.');
}

if (confirm) {
const jwt = process.env.PINATA_JWT;
Expand All @@ -47,6 +51,7 @@ export async function publishCmd(args: string[]): Promise<SubcommandResult> {
},
confirm,
name: nameOverride,
allowUnverified,
});
lines.push(` manifest CID: ${r.manifestCid}`);
lines.push(` size: ${r.manifestSize} bytes`);
Expand Down
17 changes: 17 additions & 0 deletions packages/create-agent-harness/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ export interface HarnessPublishOptions {
confirm: boolean;
/** Optional override of the harness's name (defaults to manifest.vars.name). */
name?: string;
/**
* GH #4 (HIGH-1): explicitly publish even when a present witness could NOT be cryptographically
* verified (no verifier available). Off by default — publishing is fail-closed on an unverified
* witness so a garbage signature can't masquerade as signed. The signature is NOT checked when set.
*/
allowUnverified?: boolean;
}

export interface PublishResult {
Expand Down Expand Up @@ -128,6 +134,17 @@ export async function publishHarness(opts: HarnessPublishOptions): Promise<Publi
if (!result.valid) {
throw new Error(`witness verification failed: ${result.reason ?? 'unknown'}`);
}
// GH #4 (HIGH-1): a witness whose signature was NOT cryptographically verified (shape-only
// degraded mode — no kernel verifier) must NOT be published as "signed". Before this, that path
// returned valid:true, so a shape-valid witness carrying a garbage signature published silently as
// verified. Fail closed unless the caller explicitly opts in (and is then told the sig was unchecked).
if (result.unverified && !opts.allowUnverified) {
throw new Error(
`witness present at ${witnessPath} but its signature was NOT cryptographically verified ` +
`(${result.reason ?? 'no verifier available'}). Refusing to publish this harness as signed. ` +
`Re-run with --allow-unverified-witness to publish anyway — the signature will NOT have been checked.`,
);
}
}

if (!opts.confirm) {
Expand Down
13 changes: 12 additions & 1 deletion packages/create-agent-harness/src/witness-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export interface WitnessManifest {
export interface VerificationResult {
valid: boolean;
reason?: string;
/**
* GH #4 (HIGH-1): true when the manifest passed SHAPE checks but its signature was NOT
* cryptographically verified (no kernel `witnessVerify` available — the shape-only degraded mode).
* `valid` stays true so read-only diagnostics (`verify`/`doctor`) keep reporting shape validity, but a
* caller that acts on the signature (publish) MUST treat `unverified` as "not actually signed".
*/
unverified?: boolean;
}

/**
Expand Down Expand Up @@ -83,7 +90,11 @@ export async function verifyWitness(manifest: unknown): Promise<VerificationResu
// truth; local-dev can skip the crypto verify).
}

return { valid: true, reason: 'shape verified; kernel not loaded (degraded)' };
return {
valid: true,
unverified: true,
reason: 'shape verified; kernel witnessVerify unavailable — signature NOT cryptographically checked (degraded)',
};
}

/**
Expand Down
Loading