From d8c307eed90eb1026ebadb38efc972cf90bcce4d Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 7 Jan 2025 14:14:35 -0800 Subject: [PATCH 1/2] new subject-checksums input param Signed-off-by: Brian DeHamer --- README.md | 54 +++++++++++++-- __tests__/main.test.ts | 5 +- __tests__/subject.test.ts | 138 ++++++++++++++++++++++++++++++++++++-- action.yml | 18 +++-- dist/index.js | 85 +++++++++++++++++++---- src/index.ts | 1 + src/subject.ts | 109 ++++++++++++++++++++++++++---- 7 files changed, 369 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 0bff71f1..00918027 100644 --- a/README.md +++ b/README.md @@ -74,20 +74,25 @@ See [action.yml](action.yml) - uses: actions/attest@v2 with: # Path to the artifact serving as the subject of the attestation. Must - # specify exactly one of "subject-path" or "subject-digest". May contain - # a glob pattern or list of paths (total subject count cannot exceed 1024). + # specify exactly one of "subject-path", "subject-digest", or + # "subject-checksums". May contain a glob pattern or list of paths + # (total subject count cannot exceed 1024). subject-path: # SHA256 digest of the subject for the attestation. Must be in the form # "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one - # of "subject-path" or "subject-digest". + # of "subject-path", "subject-digest", or "subject-checksums". subject-digest: - # Subject name as it should appear in the attestation. Required unless - # "subject-path" is specified, in which case it will be inferred from the - # path. + # Subject name as it should appear in the attestation. Required when + # identifying the subject with the "subject-digest" input. subject-name: + # Path to checksums file containing digest and name of subjects for + # attestation. Must specify exactly one of "subject-path", "subject-digest", + # or "subject-checksums". + subject-checksums: + # URI identifying the type of the predicate. predicate-type: @@ -209,6 +214,43 @@ newline delimited list: dist/bar ``` +### Identify Subjects with Checksums File + +If you are using tools like +[goreleaser](https://goreleaser.com/customization/checksum/) or +[jreleaser](https://jreleaser.org/guide/latest/reference/checksum.html) which +generate a checksums file you can identify the attestation subjects by passing +the path of the checksums file to the `subject-checksums` input. Each of the +artifacts identified in the checksums file will be listed as a subject for the +attestation. + +```yaml +- name: Calculate artifact digests + run: | + shasum -a 256 foo_0.0.1_* > subject.checksums.txt + +- uses: actions/attest@v2 + with: + subject-checksums: subject.checksums.txt + predicate-type: 'https://example.com/predicate/v1' + predicate: '{}' +``` + + + +The file referenced by the `subject-checksums` input must conform to the same +format used by the shasum tools. Each subject should be listed on a separate +line including the hex-encoded digest (either SHA256 or SHA512), a space, a +single character flag indicating either binary (`*`) or text (` `) input mode, +and the filename. + + + +```text +b569bf992b287f55d78bf8ee476497e9b7e9d2bf1c338860bfb905016218c740 foo_0.0.1_darwin_amd64 +a54fc515e616cac7fcf11a49d5c5ec9ec315948a5935c1e11dd610b834b14dde foo_0.0.1_darwin_arm64 +``` + ### Container Image When working with container images you can invoke the action with the diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 2b48b570..7e114d49 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -43,6 +43,7 @@ const defaultInputs: main.RunInputs = { subjectName: '', subjectDigest: '', subjectPath: '', + subjectChecksums: '', pushToRegistry: false, showSummary: true, githubToken: '', @@ -138,7 +139,9 @@ describe('action', () => { expect(runMock).toHaveReturned() expect(setFailedMock).toHaveBeenCalledWith( - new Error('One of subject-path or subject-digest must be provided') + new Error( + 'One of subject-path, subject-digest, or subject-checksums must be provided' + ) ) }) }) diff --git a/__tests__/subject.test.ts b/__tests__/subject.test.ts index da2239b8..2e54288f 100644 --- a/__tests__/subject.test.ts +++ b/__tests__/subject.test.ts @@ -12,13 +12,14 @@ describe('subjectFromInputs', () => { const blankInputs: SubjectInputs = { subjectPath: '', subjectName: '', - subjectDigest: '' + subjectDigest: '', + subjectChecksums: '' } describe('when no inputs are provided', () => { it('throws an error', async () => { await expect(subjectFromInputs(blankInputs)).rejects.toThrow( - /one of subject-path or subject-digest must be provided/i + /one of subject-path, subject-digest, or subject-checksums must be provided/i ) }) }) @@ -28,11 +29,42 @@ describe('subjectFromInputs', () => { const inputs: SubjectInputs = { subjectName: 'foo', subjectPath: 'path/to/subject', - subjectDigest: 'digest' + subjectDigest: 'digest', + subjectChecksums: '' + } + + await expect(subjectFromInputs(inputs)).rejects.toThrow( + /only one of subject-path, subject-digest, or subject-checksums may be provided/i + ) + }) + }) + + describe('when both subject path and subject checksums are provided', () => { + it('throws an error', async () => { + const inputs: SubjectInputs = { + subjectName: '', + subjectPath: 'path/to/subject', + subjectDigest: '', + subjectChecksums: 'path/to/checksums' + } + + await expect(subjectFromInputs(inputs)).rejects.toThrow( + /only one of subject-path, subject-digest, or subject-checksums may be provided/i + ) + }) + }) + + describe('when both subject digest and subject checksums are provided', () => { + it('throws an error', async () => { + const inputs: SubjectInputs = { + subjectName: 'foo', + subjectPath: '', + subjectDigest: 'digest', + subjectChecksums: 'path/to/checksums' } await expect(subjectFromInputs(inputs)).rejects.toThrow( - /only one of subject-path or subject-digest may be provided/i + /only one of subject-path, subject-digest, or subject-checksums may be provided/i ) }) }) @@ -387,6 +419,104 @@ describe('subjectFromInputs', () => { }) }) }) + + describe('when specifying a subject checksums file', () => { + const checksums = ` +187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64 +badline +5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718 demo_0.0.1_darwin_arm64` + + let dir = '' + const filename = 'checksums' + + beforeEach(async () => { + // Set-up temp directory + const tmpDir = await fs.realpath(os.tmpdir()) + dir = await fs.mkdtemp(tmpDir + path.sep) + + // Write file to temp directory + await fs.writeFile(path.join(dir, filename), checksums) + }) + + afterEach(async () => { + // Clean-up temp directory + await fs.rm(dir, { recursive: true }) + }) + + describe('when the specified path is NOT a file', () => { + it('throws an error', async () => { + const inputs: SubjectInputs = { + ...blankInputs, + subjectChecksums: dir + } + await expect(subjectFromInputs(inputs)).rejects.toThrow( + /subject checksums file not found/i + ) + }) + }) + + describe('when the specific path is a file', () => { + it('returns the multiple subjects', async () => { + const inputs: SubjectInputs = { + ...blankInputs, + subjectChecksums: path.join(dir, filename) + } + const subjects = await subjectFromInputs(inputs) + + expect(subjects).toBeDefined() + expect(subjects).toHaveLength(2) + + expect(subjects).toContainEqual({ + name: 'demo_0.0.1_linux_amd64', + digest: { + sha256: + '187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d' + } + }) + }) + }) + }) + + describe('when specifying a subject checksums string', () => { + const checksums = ` +f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386 +187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64 +9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64` + + it('returns the multiple subjects', async () => { + const inputs: SubjectInputs = { + ...blankInputs, + subjectChecksums: checksums + } + const subjects = await subjectFromInputs(inputs) + + expect(subjects).toBeDefined() + expect(subjects).toHaveLength(3) + + expect(subjects).toContainEqual({ + name: 'demo_0.0.1_linux_386', + digest: { + sha256: + 'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e' + } + }) + }) + }) + + describe('when specifying a subject checksums string with an unrecognized digest', () => { + const checksums = `f861e demo_0.0.1_linux_386` + + it('throws an error', async () => { + const inputs: SubjectInputs = { + ...blankInputs, + subjectChecksums: checksums + } + + await expect(subjectFromInputs(inputs)).rejects.toThrow( + /unknown digest algorithm/i + ) + }) + }) }) describe('subjectDigest', () => { diff --git a/action.yml b/action.yml index 6dad2ebe..bc45f792 100644 --- a/action.yml +++ b/action.yml @@ -9,20 +9,26 @@ inputs: subject-path: description: > Path to the artifact serving as the subject of the attestation. Must - specify exactly one of "subject-path" or "subject-digest". May contain a - glob pattern or list of paths (total subject count cannot exceed 1024). + specify exactly one of "subject-path", "subject-digest", or + "subject-checksums". May contain a glob pattern or list of paths (total + subject count cannot exceed 1024). required: false subject-digest: description: > Digest of the subject for the attestation. Must be in the form "algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one - of "subject-path" or "subject-digest". + of "subject-path", "subject-digest", or "subject-checksums". required: false subject-name: description: > - Subject name as it should appear in the attestation. Required unless - "subject-path" is specified, in which case it will be inferred from the - path. + Subject name as it should appear in the attestation. Required when + identifying the subject with the "subject-digest" input. + required: false + subject-checksums: + description: > + Path to checksums file containing digest and name of subjects for + attestation. Must specify exactly one of "subject-path", "subject-digest", + or "subject-checksums". required: false predicate-type: description: > diff --git a/dist/index.js b/dist/index.js index b57b7fcf..a54833ea 100644 --- a/dist/index.js +++ b/dist/index.js @@ -70835,6 +70835,7 @@ const inputs = { subjectPath: core.getInput('subject-path'), subjectName: core.getInput('subject-name'), subjectDigest: core.getInput('subject-digest'), + subjectChecksums: core.getInput('subject-checksums'), predicateType: core.getInput('predicate-type'), predicate: core.getInput('predicate'), predicatePath: core.getInput('predicate-path'), @@ -71129,23 +71130,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.formatSubjectDigest = exports.subjectFromInputs = void 0; const glob = __importStar(__nccwpck_require__(47206)); +const assert_1 = __importDefault(__nccwpck_require__(42613)); const crypto_1 = __importDefault(__nccwpck_require__(76982)); const sync_1 = __nccwpck_require__(61110); const fs_1 = __importDefault(__nccwpck_require__(79896)); +const os_1 = __importDefault(__nccwpck_require__(70857)); const path_1 = __importDefault(__nccwpck_require__(16928)); const MAX_SUBJECT_COUNT = 1024; +const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT; const DIGEST_ALGORITHM = 'sha256'; // Returns the subject specified by the action's inputs. The subject may be // specified as a path to a file or as a digest. If a path is provided, the // file's digest is calculated and returned along with the subject's name. If a // digest is provided, the name must also be provided. const subjectFromInputs = async (inputs) => { - const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs; - if (!subjectPath && !subjectDigest) { - throw new Error('One of subject-path or subject-digest must be provided'); + const { subjectPath, subjectDigest, subjectName, subjectChecksums, downcaseName } = inputs; + const enabledInputs = [subjectPath, subjectDigest, subjectChecksums].filter(Boolean); + if (enabledInputs.length === 0) { + throw new Error('One of subject-path, subject-digest, or subject-checksums must be provided'); } - if (subjectPath && subjectDigest) { - throw new Error('Only one of subject-path or subject-digest may be provided'); + if (enabledInputs.length > 1) { + throw new Error('Only one of subject-path, subject-digest, or subject-checksums may be provided'); } if (subjectDigest && !subjectName) { throw new Error('subject-name must be provided when using subject-digest'); @@ -71153,11 +71158,17 @@ const subjectFromInputs = async (inputs) => { // If push-to-registry is enabled, ensure the subject name is lowercase // to conform to OCI image naming conventions const name = downcaseName ? subjectName.toLowerCase() : subjectName; - if (subjectPath) { - return await getSubjectFromPath(subjectPath, name); - } - else { - return [getSubjectFromDigest(subjectDigest, name)]; + switch (true) { + case !!subjectPath: + return getSubjectFromPath(subjectPath, name); + case !!subjectDigest: + return [getSubjectFromDigest(subjectDigest, name)]; + case !!subjectChecksums: + return getSubjectFromChecksums(subjectChecksums); + /* istanbul ignore next */ + default: + // This should be unreachable, but TS requires a default case + assert_1.default.fail('unreachable'); } }; exports.subjectFromInputs = subjectFromInputs; @@ -71173,7 +71184,7 @@ exports.formatSubjectDigest = formatSubjectDigest; const getSubjectFromPath = async (subjectPath, subjectName) => { const digestedSubjects = []; // Parse the list of subject paths - const subjectPaths = parseList(subjectPath).join('\n'); + const subjectPaths = parseSubjectPathList(subjectPath).join('\n'); // Expand the globbed paths to a list of actual paths const paths = await glob.create(subjectPaths).then(async (g) => g.glob()); // Filter path list to just the files (not directories) @@ -71206,6 +71217,46 @@ const getSubjectFromDigest = (subjectDigest, subjectName) => { digest: { [alg]: digest } }; }; +const getSubjectFromChecksums = (subjectChecksums) => { + if (fs_1.default.existsSync(subjectChecksums)) { + return getSubjectFromChecksumsFile(subjectChecksums); + } + else { + return getSubjectFromChecksumsString(subjectChecksums); + } +}; +const getSubjectFromChecksumsFile = (checksumsPath) => { + const stats = fs_1.default.statSync(checksumsPath); + if (!stats.isFile()) { + throw new Error(`subject checksums file not found: ${checksumsPath}`); + } + /* istanbul ignore next */ + if (stats.size > MAX_SUBJECT_CHECKSUM_SIZE_BYTES) { + throw new Error(`subject checksums file exceeds maximum allowed size: ${MAX_SUBJECT_CHECKSUM_SIZE_BYTES} bytes`); + } + const checksums = fs_1.default.readFileSync(checksumsPath, 'utf-8'); + return getSubjectFromChecksumsString(checksums); +}; +const getSubjectFromChecksumsString = (checksums) => { + const subjects = []; + const records = checksums.split(os_1.default.EOL).filter(Boolean); + for (const record of records) { + // Find the space delimiter following the digest + const delimIndex = record.indexOf(' '); + // Skip any line that doesn't have a delimiter + if (delimIndex === -1) { + continue; + } + // Swallow the type identifier character at the beginning of the name + const name = record.slice(delimIndex + 2); + const digest = record.slice(0, delimIndex); + subjects.push({ + name, + digest: { [digestAlgorithm(digest)]: digest } + }); + } + return subjects; +}; // Calculates the digest of a file using the specified algorithm. The file is // streamed into the digest function to avoid loading the entire file into // memory. The returned digest is a hex string. @@ -71218,7 +71269,7 @@ const digestFile = async (algorithm, filePath) => { .once('finish', () => resolve(hash.read())); }); }; -const parseList = (input) => { +const parseSubjectPathList = (input) => { const res = []; const records = (0, sync_1.parse)(input, { columns: false, @@ -71231,6 +71282,16 @@ const parseList = (input) => { } return res.filter(item => item).map(pat => pat.trim()); }; +const digestAlgorithm = (digest) => { + switch (digest.length) { + case 64: + return 'sha256'; + case 128: + return 'sha512'; + default: + throw new Error(`Unknown digest algorithm: ${digest}`); + } +}; /***/ }), diff --git a/src/index.ts b/src/index.ts index d23ee2b3..a4de2b8c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ const inputs: RunInputs = { subjectPath: core.getInput('subject-path'), subjectName: core.getInput('subject-name'), subjectDigest: core.getInput('subject-digest'), + subjectChecksums: core.getInput('subject-checksums'), predicateType: core.getInput('predicate-type'), predicate: core.getInput('predicate'), predicatePath: core.getInput('predicate-path'), diff --git a/src/subject.ts b/src/subject.ts index 95cbf3f5..1c88a45c 100644 --- a/src/subject.ts +++ b/src/subject.ts @@ -1,18 +1,22 @@ import * as glob from '@actions/glob' +import assert from 'assert' import crypto from 'crypto' import { parse } from 'csv-parse/sync' import fs from 'fs' +import os from 'os' import path from 'path' import type { Subject } from '@actions/attest' const MAX_SUBJECT_COUNT = 1024 +const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT const DIGEST_ALGORITHM = 'sha256' export type SubjectInputs = { subjectPath: string subjectName: string subjectDigest: string + subjectChecksums: string downcaseName?: boolean } // Returns the subject specified by the action's inputs. The subject may be @@ -22,15 +26,26 @@ export type SubjectInputs = { export const subjectFromInputs = async ( inputs: SubjectInputs ): Promise => { - const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs - - if (!subjectPath && !subjectDigest) { - throw new Error('One of subject-path or subject-digest must be provided') + const { + subjectPath, + subjectDigest, + subjectName, + subjectChecksums, + downcaseName + } = inputs + + const enabledInputs = [subjectPath, subjectDigest, subjectChecksums].filter( + Boolean + ) + if (enabledInputs.length === 0) { + throw new Error( + 'One of subject-path, subject-digest, or subject-checksums must be provided' + ) } - if (subjectPath && subjectDigest) { + if (enabledInputs.length > 1) { throw new Error( - 'Only one of subject-path or subject-digest may be provided' + 'Only one of subject-path, subject-digest, or subject-checksums may be provided' ) } @@ -42,10 +57,17 @@ export const subjectFromInputs = async ( // to conform to OCI image naming conventions const name = downcaseName ? subjectName.toLowerCase() : subjectName - if (subjectPath) { - return await getSubjectFromPath(subjectPath, name) - } else { - return [getSubjectFromDigest(subjectDigest, name)] + switch (true) { + case !!subjectPath: + return getSubjectFromPath(subjectPath, name) + case !!subjectDigest: + return [getSubjectFromDigest(subjectDigest, name)] + case !!subjectChecksums: + return getSubjectFromChecksums(subjectChecksums) + /* istanbul ignore next */ + default: + // This should be unreachable, but TS requires a default case + assert.fail('unreachable') } } @@ -65,7 +87,7 @@ const getSubjectFromPath = async ( const digestedSubjects: Subject[] = [] // Parse the list of subject paths - const subjectPaths = parseList(subjectPath).join('\n') + const subjectPaths = parseSubjectPathList(subjectPath).join('\n') // Expand the globbed paths to a list of actual paths const paths = await glob.create(subjectPaths).then(async g => g.glob()) @@ -119,6 +141,58 @@ const getSubjectFromDigest = ( } } +const getSubjectFromChecksums = (subjectChecksums: string): Subject[] => { + if (fs.existsSync(subjectChecksums)) { + return getSubjectFromChecksumsFile(subjectChecksums) + } else { + return getSubjectFromChecksumsString(subjectChecksums) + } +} + +const getSubjectFromChecksumsFile = (checksumsPath: string): Subject[] => { + const stats = fs.statSync(checksumsPath) + if (!stats.isFile()) { + throw new Error(`subject checksums file not found: ${checksumsPath}`) + } + + /* istanbul ignore next */ + if (stats.size > MAX_SUBJECT_CHECKSUM_SIZE_BYTES) { + throw new Error( + `subject checksums file exceeds maximum allowed size: ${MAX_SUBJECT_CHECKSUM_SIZE_BYTES} bytes` + ) + } + + const checksums = fs.readFileSync(checksumsPath, 'utf-8') + return getSubjectFromChecksumsString(checksums) +} + +const getSubjectFromChecksumsString = (checksums: string): Subject[] => { + const subjects: Subject[] = [] + + const records: string[] = checksums.split(os.EOL).filter(Boolean) + + for (const record of records) { + // Find the space delimiter following the digest + const delimIndex = record.indexOf(' ') + + // Skip any line that doesn't have a delimiter + if (delimIndex === -1) { + continue + } + + // Swallow the type identifier character at the beginning of the name + const name = record.slice(delimIndex + 2) + const digest = record.slice(0, delimIndex) + + subjects.push({ + name, + digest: { [digestAlgorithm(digest)]: digest } + }) + } + + return subjects +} + // Calculates the digest of a file using the specified algorithm. The file is // streamed into the digest function to avoid loading the entire file into // memory. The returned digest is a hex string. @@ -135,7 +209,7 @@ const digestFile = async ( }) } -const parseList = (input: string): string[] => { +const parseSubjectPathList = (input: string): string[] => { const res: string[] = [] const records: string[][] = parse(input, { @@ -151,3 +225,14 @@ const parseList = (input: string): string[] => { return res.filter(item => item).map(pat => pat.trim()) } + +const digestAlgorithm = (digest: string): string => { + switch (digest.length) { + case 64: + return 'sha256' + case 128: + return 'sha512' + default: + throw new Error(`Unknown digest algorithm: ${digest}`) + } +} From f5612513d6018c85eeeb591d55b336b999dca8f2 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Thu, 16 Jan 2025 08:07:37 -0800 Subject: [PATCH 2/2] check for valid hex string for digest Signed-off-by: Brian DeHamer --- __tests__/subject.test.ts | 14 ++++++++++++++ dist/index.js | 4 ++++ src/subject.ts | 5 +++++ 3 files changed, 23 insertions(+) diff --git a/__tests__/subject.test.ts b/__tests__/subject.test.ts index 2e54288f..c0d89e48 100644 --- a/__tests__/subject.test.ts +++ b/__tests__/subject.test.ts @@ -517,6 +517,20 @@ f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_lin ) }) }) + + describe('when specifying a subject checksums string with an invalid digest', () => { + const checksums = + '!!!!e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386' + + it('throws an error', async () => { + const inputs: SubjectInputs = { + ...blankInputs, + subjectChecksums: checksums + } + + await expect(subjectFromInputs(inputs)).rejects.toThrow(/invalid digest/i) + }) + }) }) describe('subjectDigest', () => { diff --git a/dist/index.js b/dist/index.js index a54833ea..a188c1f0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -71139,6 +71139,7 @@ const path_1 = __importDefault(__nccwpck_require__(16928)); const MAX_SUBJECT_COUNT = 1024; const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT; const DIGEST_ALGORITHM = 'sha256'; +const HEX_STRING_RE = /^[0-9a-fA-F]+$/; // Returns the subject specified by the action's inputs. The subject may be // specified as a path to a file or as a digest. If a path is provided, the // file's digest is calculated and returned along with the subject's name. If a @@ -71250,6 +71251,9 @@ const getSubjectFromChecksumsString = (checksums) => { // Swallow the type identifier character at the beginning of the name const name = record.slice(delimIndex + 2); const digest = record.slice(0, delimIndex); + if (!HEX_STRING_RE.test(digest)) { + throw new Error(`Invalid digest: ${digest}`); + } subjects.push({ name, digest: { [digestAlgorithm(digest)]: digest } diff --git a/src/subject.ts b/src/subject.ts index 1c88a45c..b977f18f 100644 --- a/src/subject.ts +++ b/src/subject.ts @@ -11,6 +11,7 @@ import type { Subject } from '@actions/attest' const MAX_SUBJECT_COUNT = 1024 const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT const DIGEST_ALGORITHM = 'sha256' +const HEX_STRING_RE = /^[0-9a-fA-F]+$/ export type SubjectInputs = { subjectPath: string @@ -184,6 +185,10 @@ const getSubjectFromChecksumsString = (checksums: string): Subject[] => { const name = record.slice(delimIndex + 2) const digest = record.slice(0, delimIndex) + if (!HEX_STRING_RE.test(digest)) { + throw new Error(`Invalid digest: ${digest}`) + } + subjects.push({ name, digest: { [digestAlgorithm(digest)]: digest }