Skip to content
Closed
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
24 changes: 17 additions & 7 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72224,21 +72224,24 @@ async function ensureAuthsInstalled(version) {
}
// Determine the version for cache lookup
const cacheVersion = version;
// Check tool cache
// Determine the download URL early. It is needed both to download the binary and
// to re-verify any cached binary against the release checksum — the cache is
// untrusted, so a cache hit is not a substitute for verification.
const downloadUrl = getAuthsDownloadUrl(version);
if (!downloadUrl) {
core.warning(`Cannot determine auths download URL for this platform (${os.platform()}/${os.arch()})`);
return null;
}
// Check tool cache — re-verify before trusting it.
const cachedPath = tc.find('auths', cacheVersion);
if (cachedPath) {
const binaryPath = path.join(cachedPath, binaryName);
if (fs.existsSync(binaryPath)) {
await verifyChecksum(downloadUrl, binaryPath);
core.info(`Using cached auths: ${binaryPath}`);
return binaryPath;
}
}
// Determine download URL early (needed for cache key)
const downloadUrl = getAuthsDownloadUrl(version);
if (!downloadUrl) {
core.warning(`Cannot determine auths download URL for this platform (${os.platform()}/${os.arch()})`);
return null;
}
// Try cross-run cache (only for pinned versions — "latest" can change between runs)
const useCrossRunCache = version.length > 0;
const urlHash = crypto.createHash('sha256').update(downloadUrl).digest('hex').slice(0, 16);
Expand All @@ -72251,12 +72254,19 @@ async function ensureAuthsInstalled(version) {
core.info(`Restored auths from cache (key: ${cacheKey})`);
const restoredBinary = path.join(cachePaths[0], binaryName);
if (fs.existsSync(restoredBinary)) {
await verifyChecksum(downloadUrl, restoredBinary);
const cachedDir = await tc.cacheDir(cachePaths[0], 'auths', cacheVersion);
return path.join(cachedDir, binaryName);
}
}
}
catch (e) {
// A checksum failure on a restored binary means the cache entry was tampered
// with — surface it, never swallow it as a routine cache miss. Only genuine
// cache I/O errors fall through to a fresh, verified download.
if (e instanceof Error && /checksum|unverified binary/i.test(e.message)) {
throw e;
}
core.debug(`Cache restore failed (non-fatal): ${e}`);
}
}
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

57 changes: 53 additions & 4 deletions src/__tests__/verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,21 +295,70 @@ describe('ensureAuthsInstalled - cross-run caching', () => {
}
});

it('restores from cache on hit', async () => {
it('restores from cache on hit and re-verifies the binary', async () => {
// Set up: cache restore returns a key hit with a binary on disk
fs.mkdirSync(cachePath, { recursive: true });
fs.writeFileSync(path.join(cachePath, 'auths'), 'binary-content');
const cachedBinary = path.join(cachePath, 'auths');
fs.writeFileSync(cachedBinary, 'binary-content');

// A matching release checksum so the re-verification passes.
const checksumFile = path.join(realTmpdir, 'auths-cache-ok.sha256');
const hash = crypto.createHash('sha256').update(fs.readFileSync(cachedBinary)).digest('hex');
fs.writeFileSync(checksumFile, `${hash} auths\n`);

mockCache.restoreCache.mockResolvedValue('auths-bin-linux-x64-abc123');
mockTc.downloadTool.mockResolvedValue(checksumFile);
mockTc.cacheDir.mockResolvedValue('/tool-cache/auths/0.5.0');

const result = await ensureAuthsInstalled('0.5.0');

expect(mockCache.restoreCache).toHaveBeenCalledTimes(1);
expect(mockTc.cacheDir).toHaveBeenCalledWith(cachePath, 'auths', '0.5.0');
// Download should NOT be called
expect(mockTc.downloadTool).not.toHaveBeenCalled();
// The cached binary is re-verified against the release checksum before use.
expect(mockTc.downloadTool).toHaveBeenCalled();
expect(result).toBe('/tool-cache/auths/0.5.0/auths');

if (fs.existsSync(checksumFile)) fs.rmSync(checksumFile);
});

it('re-verifies a cache hit and rejects a tampered binary (not trusted blind)', async () => {
// A cross-run cache hit whose restored binary does NOT match the release checksum.
fs.mkdirSync(cachePath, { recursive: true });
fs.writeFileSync(path.join(cachePath, 'auths'), 'tampered-binary');

mockCache.restoreCache.mockResolvedValue('auths-bin-linux-x64-abc123');
mockTc.cacheDir.mockResolvedValue('/tool-cache/auths/0.5.0');

// The release .sha256 does not match the tampered cached binary.
const checksumFile = path.join(realTmpdir, 'auths-cache-tampered.sha256');
fs.writeFileSync(
checksumFile,
'deadbeef00000000000000000000000000000000000000000000000000000000 auths\n'
);
mockTc.downloadTool.mockResolvedValue(checksumFile);

await expect(ensureAuthsInstalled('0.5.0')).rejects.toThrow(/checksum mismatch/i);

if (fs.existsSync(checksumFile)) fs.rmSync(checksumFile);
});

it('re-verifies a tool-cache hit and rejects a tampered binary', async () => {
const toolCacheDir = path.join(realTmpdir, 'auths-toolcache');
fs.mkdirSync(toolCacheDir, { recursive: true });
fs.writeFileSync(path.join(toolCacheDir, 'auths'), 'tampered-binary');
mockTc.find.mockReturnValue(toolCacheDir);

const checksumFile = path.join(realTmpdir, 'auths-toolcache-tampered.sha256');
fs.writeFileSync(
checksumFile,
'deadbeef00000000000000000000000000000000000000000000000000000000 auths\n'
);
mockTc.downloadTool.mockResolvedValue(checksumFile);

await expect(ensureAuthsInstalled('0.5.0')).rejects.toThrow(/checksum mismatch/i);

if (fs.existsSync(toolCacheDir)) fs.rmSync(toolCacheDir, { recursive: true });
if (fs.existsSync(checksumFile)) fs.rmSync(checksumFile);
});

it('downloads and saves to cache on miss', async () => {
Expand Down
26 changes: 18 additions & 8 deletions src/verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,23 +418,26 @@ export async function ensureAuthsInstalled(version: string): Promise<string | nu
// Determine the version for cache lookup
const cacheVersion = version;

// Check tool cache
// Determine the download URL early. It is needed both to download the binary and
// to re-verify any cached binary against the release checksum — the cache is
// untrusted, so a cache hit is not a substitute for verification.
const downloadUrl = getAuthsDownloadUrl(version);
if (!downloadUrl) {
core.warning(`Cannot determine auths download URL for this platform (${os.platform()}/${os.arch()})`);
return null;
}

// Check tool cache — re-verify before trusting it.
const cachedPath = tc.find('auths', cacheVersion);
if (cachedPath) {
const binaryPath = path.join(cachedPath, binaryName);
if (fs.existsSync(binaryPath)) {
await verifyChecksum(downloadUrl, binaryPath);
core.info(`Using cached auths: ${binaryPath}`);
return binaryPath;
}
}

// Determine download URL early (needed for cache key)
const downloadUrl = getAuthsDownloadUrl(version);
if (!downloadUrl) {
core.warning(`Cannot determine auths download URL for this platform (${os.platform()}/${os.arch()})`);
return null;
}

// Try cross-run cache (only for pinned versions — "latest" can change between runs)
const useCrossRunCache = version.length > 0;
const urlHash = crypto.createHash('sha256').update(downloadUrl).digest('hex').slice(0, 16);
Expand All @@ -448,11 +451,18 @@ export async function ensureAuthsInstalled(version: string): Promise<string | nu
core.info(`Restored auths from cache (key: ${cacheKey})`);
const restoredBinary = path.join(cachePaths[0], binaryName);
if (fs.existsSync(restoredBinary)) {
await verifyChecksum(downloadUrl, restoredBinary);
const cachedDir = await tc.cacheDir(cachePaths[0], 'auths', cacheVersion);
return path.join(cachedDir, binaryName);
}
}
} catch (e) {
// A checksum failure on a restored binary means the cache entry was tampered
// with — surface it, never swallow it as a routine cache miss. Only genuine
// cache I/O errors fall through to a fresh, verified download.
if (e instanceof Error && /checksum|unverified binary/i.test(e.message)) {
throw e;
}
core.debug(`Cache restore failed (non-fatal): ${e}`);
}
}
Expand Down
Loading