diff --git a/TODO.md b/TODO.md index 3d7d1cc..aceac35 100644 --- a/TODO.md +++ b/TODO.md @@ -25,7 +25,7 @@ | NFR-3 | exact 指定時の一覧取得スキップ + cache 優先 | `src/version.ts` / `src/main.ts` | | §10.1 | ユニットテスト(version / platform / github / download / checksum) | `tests/*.test.ts` | | §10.2 | cache-hit 経路の self-test | `.github/workflows/self-test.yml` | -| §10.3 | checksum 改ざん検出テスト | `tests/checksum*.test.ts` | +| §10.3 | checksum 改ざん検出テスト | `tests/checksum.test.ts` | ### 未対応・進行中 @@ -52,7 +52,6 @@ | [#39](https://github.com/yk-lab/setup-task/issues/39) | `[enhancement]` ジョブサマリに導入結果を出力(NFR-5) | `P3: low` | `enhancement` | | [#41](https://github.com/yk-lab/setup-task/issues/41) | `[test]` platform.test.ts を §9 全 os/arch 組合せに拡張 | `P3: low` | `test` | | [#42](https://github.com/yk-lab/setup-task/issues/42) | `[security]` ダウンロード先ホスト/リダイレクト先を検証(NFR-1) | `P2: medium` | `security` | -| [#43](https://github.com/yk-lab/setup-task/issues/43) | `[chore]` checksum 改ざんテストの重複ファイルを統合 | `P3: low` | `chore` `test` | | [#44](https://github.com/yk-lab/setup-task/issues/44) | `[enhancement]` フォールバックソースをサポート(FR-11) | `P3: low` | `enhancement` | | [#45](https://github.com/yk-lab/setup-task/issues/45) | `[chore]` Biome 導入を評価 | `P3: low` | `chore` | @@ -77,3 +76,4 @@ v1.0.0 実装中に作成・解決済みの Issue(参考)。 | [#7](https://github.com/yk-lab/setup-task/issues/7) | `[bug]` レンジ指定時に tool-cache より先に GitHub 解決する | #30 | | [#22](https://github.com/yk-lab/setup-task/issues/22) | dist/ freshness 管理 | #36(main から dist/ を外し、リリース時ビルド方式へ) | | [#40](https://github.com/yk-lab/setup-task/issues/40) | `[enhancement]` リトライ回数・間隔を input 化(FR-4) | #51 | +| [#43](https://github.com/yk-lab/setup-task/issues/43) | `[chore]` checksum 改ざんテストの重複ファイルを統合 | #52 | diff --git a/tests/checksum-tamper.test.ts b/tests/checksum-tamper.test.ts deleted file mode 100644 index 59b4faf..0000000 --- a/tests/checksum-tamper.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as crypto from 'node:crypto'; -import * as fs from 'node:fs'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { fetchChecksum, verifyChecksum } from '../src/checksum'; -import { PermanentError } from '../src/errors'; - -/** - * Integration test for the checksum pipeline (FR-5, 要求仕様書 §10.3): exercise - * fetchChecksum (network + parse) and verifyChecksum (hash + compare) together - * and prove that a tampered archive is rejected. The action hard-codes its - * download source (CON-2), so the malicious response is injected by stubbing - * fetch rather than by pointing the action at another URL. - */ - -const ASSET = 'task_linux_amd64.tar.gz'; - -function stubChecksumsBody(body: string): void { - vi.stubGlobal( - 'fetch', - vi.fn(async () => new Response(body, { status: 200, headers: { 'content-type': 'text/plain' } })), - ); -} - -let tmpCounter = 0; -function writeTmp(content: Buffer): string { - const p = path.join(os.tmpdir(), `setup-task-tamper-${process.pid}-${tmpCounter++}.bin`); - fs.writeFileSync(p, content); - return p; -} - -describe('checksum tamper detection (fetch + parse + verify)', () => { - afterEach(() => vi.unstubAllGlobals()); - - const genuine = Buffer.from('genuine task archive payload'); - const genuineSha = crypto.createHash('sha256').update(genuine).digest('hex'); - const checksumsBody = [ - `${genuineSha} ${ASSET}`, - `${'0'.repeat(64)} task_darwin_arm64.tar.gz`, - ].join('\n'); - - it('rejects a tampered archive against the published checksum', async () => { - stubChecksumsBody(checksumsBody); - const expected = await fetchChecksum('v3.51.1', ASSET); - expect(expected).toBe(genuineSha); - if (!expected) return; - - const tampered = writeTmp(Buffer.concat([genuine, Buffer.from('!!injected')])); - try { - // A mismatch is a permanent, security-relevant failure (never retried). - expect(() => verifyChecksum(tampered, expected)).toThrow(PermanentError); - expect(() => verifyChecksum(tampered, expected)).toThrow(/Checksum mismatch/); - } finally { - fs.rmSync(tampered, { force: true }); - } - }); - - it('accepts a genuine archive matching the published checksum', async () => { - stubChecksumsBody(checksumsBody); - const expected = await fetchChecksum('v3.51.1', ASSET); - expect(expected).toBe(genuineSha); - if (!expected) return; - - const file = writeTmp(genuine); - try { - expect(() => verifyChecksum(file, expected)).not.toThrow(); - } finally { - fs.rmSync(file, { force: true }); - } - }); - - it('returns undefined when the asset is absent from the checksums file', async () => { - stubChecksumsBody(checksumsBody); - await expect(fetchChecksum('v3.51.1', 'task_freebsd_riscv64.tar.gz')).resolves.toBeUndefined(); - }); -});