-
Notifications
You must be signed in to change notification settings - Fork 100
feat(integrity): SHA-256 skill integrity verification (#90 phase 1) #122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to SkillKit are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions use [Semantic Versioning](https://semver.org/). | ||
|
|
||
| ## [Unreleased] | ||
|
|
||
| ### Added | ||
| - **Skill integrity verification (`skillkit verify`).** New command computes a deterministic SHA-256 over every file in a skill (excluding metadata, VCS, and editor artifacts) and emits the result as a Subresource Integrity string (`sha256-<base64>`). Supports: | ||
| - `--expected <sri|hex>` to verify against a known publisher digest | ||
| - `--against-lock` to verify against the local `~/.skillkit/lock.json` entry | ||
| - `--files` to dump per-file digests | ||
| - `--json` for CI pipelines | ||
| - **`computeSkillIntegrity` / `verifySkillIntegrity`** exported from `@skillkit/core` for programmatic use. | ||
| - **`integrity` field** in `WellKnownSkill` index. `skillkit publish` now records a full SRI digest for each published skill alongside the file list. | ||
| - **Lockfile records full integrity.** `skillkit install` and `skillkit update` now persist the full SRI digest to `~/.skillkit/lock.json` (with graceful fallback to the legacy short checksum when integrity computation fails). | ||
| - Phase 1 of supply-chain integrity (issue #90). Phase 2 (ed25519 signing + publisher key registry) tracked separately. | ||
|
|
||
| ### Tests | ||
| - 11 unit tests for the integrity module (`packages/core/src/integrity/__tests__/integrity.test.ts`). | ||
| - 5 CLI end-to-end tests for `skillkit verify` (`packages/cli/src/__tests__/verify.test.ts`). | ||
|
|
||
| ## [1.24.0] - 2026-04-XX | ||
|
|
||
| See [GitHub releases](https://github.com/rohitg00/skillkit/releases) for prior history. | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -99,6 +99,9 @@ const COMMAND_GROUPS: CommandGroup[] = [ | |||||||||
| { cmd: 'scan --format sarif', desc: 'SARIF output' }, | ||||||||||
| { cmd: 'scan --fail-on high', desc: 'CI gate' }, | ||||||||||
| { cmd: 'install --no-scan', desc: 'Skip scan' }, | ||||||||||
| { cmd: 'verify <path>', desc: 'SHA-256 integrity (SRI)' }, | ||||||||||
| { cmd: 'verify --expected sha256-...', desc: 'Match publisher digest' }, | ||||||||||
| { cmd: 'verify --against-lock', desc: 'Verify vs lockfile' }, | ||||||||||
|
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include the required These two entries are missing the required positional path argument, so the displayed commands are not executable as shown. 💡 Suggested fix- { cmd: 'verify --expected sha256-...', desc: 'Match publisher digest' },
- { cmd: 'verify --against-lock', desc: 'Verify vs lockfile' },
+ { cmd: 'verify <path> --expected sha256-...', desc: 'Match publisher digest' },
+ { cmd: 'verify <path> --against-lock', desc: 'Verify vs lockfile' },📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| { cmd: 'audit log', desc: 'View audit logs' }, | ||||||||||
| { cmd: 'validate', desc: 'Validate format' }, | ||||||||||
| ], | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | ||
| import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { execFileSync } from 'node:child_process'; | ||
|
|
||
| const CLI = join(__dirname, '../../../../apps/skillkit/dist/cli.js'); | ||
|
|
||
| function run(args: string[]): { code: number; stdout: string; stderr: string } { | ||
| try { | ||
| const stdout = execFileSync(process.execPath, [CLI, ...args], { | ||
| encoding: 'utf-8', | ||
| stdio: ['ignore', 'pipe', 'pipe'], | ||
| }); | ||
| return { code: 0, stdout, stderr: '' }; | ||
| } catch (err: any) { | ||
| return { | ||
| code: err.status ?? 1, | ||
| stdout: err.stdout?.toString() ?? '', | ||
| stderr: err.stderr?.toString() ?? '', | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| describe('skillkit verify', () => { | ||
| let dir: string; | ||
|
|
||
| beforeEach(() => { | ||
| dir = mkdtempSync(join(tmpdir(), 'skillkit-verify-cli-')); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| rmSync(dir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('prints an SRI digest by default', () => { | ||
| writeFileSync(join(dir, 'SKILL.md'), '# test\n'); | ||
| const r = run(['verify', dir]); | ||
| expect(r.code).toBe(0); | ||
| expect(r.stdout.trim()).toMatch(/^sha256-[A-Za-z0-9+/]+=*$/); | ||
| }); | ||
|
|
||
| it('emits JSON with --json', () => { | ||
| writeFileSync(join(dir, 'SKILL.md'), '# test\n'); | ||
| mkdirSync(join(dir, 'scripts')); | ||
| writeFileSync(join(dir, 'scripts', 'a.sh'), 'echo'); | ||
| const r = run(['verify', dir, '--json']); | ||
| expect(r.code).toBe(0); | ||
| const payload = JSON.parse(r.stdout); | ||
| expect(payload.ok).toBe(true); | ||
| expect(payload.algorithm).toBe('sha256'); | ||
| expect(payload.sri).toMatch(/^sha256-/); | ||
| expect(typeof payload.digest).toBe('string'); | ||
| expect(payload.totalBytes).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it('returns exit 1 on integrity mismatch', () => { | ||
| writeFileSync(join(dir, 'SKILL.md'), 'hello'); | ||
| const bogus = 'sha256-' + Buffer.alloc(32, 7).toString('base64'); | ||
| const r = run(['verify', dir, '--expected', bogus, '--json']); | ||
| expect(r.code).toBe(1); | ||
| const payload = JSON.parse(r.stdout); | ||
| expect(payload.ok).toBe(false); | ||
| expect(payload.reason).toBe('mismatch'); | ||
| }); | ||
|
|
||
| it('returns exit 0 when --expected matches', () => { | ||
| writeFileSync(join(dir, 'SKILL.md'), 'matched'); | ||
| const computed = run(['verify', dir]); | ||
| const sri = computed.stdout.trim(); | ||
| const r = run(['verify', dir, '--expected', sri, '--json']); | ||
| expect(r.code).toBe(0); | ||
| const payload = JSON.parse(r.stdout); | ||
| expect(payload.ok).toBe(true); | ||
| }); | ||
|
|
||
| it('rejects garbage --expected', () => { | ||
| writeFileSync(join(dir, 'SKILL.md'), 'x'); | ||
| const r = run(['verify', dir, '--expected', 'garbage']); | ||
| expect(r.code).toBe(1); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace placeholder release date before merge.
Line 22 still has
2026-04-XX. For a versioned changelog entry, this should be a concrete date (or removed until release) to avoid ambiguous release metadata.🤖 Prompt for AI Agents