From 265a5be8bc69fbea621091c2f8f5b08586fa383c Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Wed, 30 Oct 2024 10:55:36 -0700 Subject: [PATCH] support multi-subject attestations Signed-off-by: Brian DeHamer --- packages/attest/README.md | 34 ++++++++++++-------- packages/attest/__tests__/attest.test.ts | 16 +++++++++ packages/attest/__tests__/intoto.test.ts | 2 +- packages/attest/__tests__/provenance.test.ts | 12 +++---- packages/attest/src/attest.ts | 32 ++++++++++++------ packages/attest/src/intoto.ts | 4 +-- 6 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 packages/attest/__tests__/attest.test.ts diff --git a/packages/attest/README.md b/packages/attest/README.md index 8f004399..e6761ea6 100644 --- a/packages/attest/README.md +++ b/packages/attest/README.md @@ -32,8 +32,7 @@ async function run() { const ghToken = core.getInput('gh-token'); const attestation = await attest({ - subjectName: 'my-artifact-name', - subjectDigest: { 'sha256': '36ab4667...'}, + subjects: [{name: 'my-artifact-name', digest: { 'sha256': '36ab4667...'}}], predicateType: 'https://in-toto.io/attestation/release', predicate: { . . . }, token: ghToken @@ -49,11 +48,12 @@ The `attest` function supports the following options: ```typescript export type AttestOptions = { - // The name of the subject to be attested. - subjectName: string - // The digest of the subject to be attested. Should be a map of digest - // algorithms to their hex-encoded values. - subjectDigest: Record + // Deprecated. Use 'subjects' instead. + subjectName?: string + // Deprecated. Use 'subjects' instead. + subjectDigest?: Record + // Collection of subjects to be attested + subjects?: Subject[] // URI identifying the content type of the predicate being attested. predicateType: string // Predicate to be attested. @@ -68,6 +68,13 @@ export type AttestOptions = { // Whether to skip writing the attestation to the GH attestations API. skipWrite?: boolean } + +export type Subject = { + // Name of the subject. + name: string + // Digests of the subject. Should be a map of digest algorithms to their hex-encoded values. + digest: Record +} ``` ### `attestProvenance` @@ -105,12 +112,13 @@ The `attestProvenance` function supports the following options: ```typescript export type AttestProvenanceOptions = { - // The name of the subject to be attested. - subjectName: string - // The digest of the subject to be attested. Should be a map of digest - // algorithms to their hex-encoded values. - subjectDigest: Record - // GitHub token for writing attestations. + // Deprecated. Use 'subjects' instead. + subjectName?: string + // Deprecated. Use 'subjects' instead. + subjectDigest?: Record + // Collection of subjects to be attested + subjects?: Subject[] + // URI identifying the content type of the predicate being attested. token: string // Sigstore instance to use for signing. Must be one of "public-good" or // "github". diff --git a/packages/attest/__tests__/attest.test.ts b/packages/attest/__tests__/attest.test.ts new file mode 100644 index 00000000..d8b07163 --- /dev/null +++ b/packages/attest/__tests__/attest.test.ts @@ -0,0 +1,16 @@ +import {attest} from '../src/attest' + +describe('attest', () => { + describe('when no subject information is provided', () => { + it('throws an error', async () => { + const options = { + predicateType: 'foo', + predicate: {bar: 'baz'}, + token: 'token' + } + expect(attest(options)).rejects.toThrowError( + 'Must provide either subjectName and subjectDigest or subjects' + ) + }) + }) +}) diff --git a/packages/attest/__tests__/intoto.test.ts b/packages/attest/__tests__/intoto.test.ts index dd6a1a95..c69f7d84 100644 --- a/packages/attest/__tests__/intoto.test.ts +++ b/packages/attest/__tests__/intoto.test.ts @@ -17,7 +17,7 @@ describe('buildIntotoStatement', () => { } it('returns an intoto statement', () => { - const statement = buildIntotoStatement(subject, predicate) + const statement = buildIntotoStatement([subject], predicate) expect(statement).toMatchSnapshot() }) }) diff --git a/packages/attest/__tests__/provenance.test.ts b/packages/attest/__tests__/provenance.test.ts index 4dbfef58..cca7a020 100644 --- a/packages/attest/__tests__/provenance.test.ts +++ b/packages/attest/__tests__/provenance.test.ts @@ -115,8 +115,7 @@ describe('provenance functions', () => { describe('when the sigstore instance is explicitly set', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token', sigstore: 'github' }) @@ -143,8 +142,7 @@ describe('provenance functions', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token' }) @@ -178,8 +176,7 @@ describe('provenance functions', () => { describe('when the sigstore instance is explicitly set', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token', sigstore: 'public-good' }) @@ -206,8 +203,7 @@ describe('provenance functions', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token' }) diff --git a/packages/attest/src/attest.ts b/packages/attest/src/attest.ts index 85c63013..807a8e5d 100644 --- a/packages/attest/src/attest.ts +++ b/packages/attest/src/attest.ts @@ -14,11 +14,16 @@ const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json' * Options for attesting a subject / predicate. */ export type AttestOptions = { - // The name of the subject to be attested. - subjectName: string - // The digest of the subject to be attested. Should be a map of digest - // algorithms to their hex-encoded values. - subjectDigest: Record + /** + * @deprecated Use `subjects` instead. + **/ + subjectName?: string + /** + * @deprecated Use `subjects` instead. + **/ + subjectDigest?: Record + // Subjects to be attested. + subjects?: Subject[] // Content type of the predicate being attested. predicateType: string // Predicate to be attested. @@ -42,15 +47,24 @@ export type AttestOptions = { * @returns A promise that resolves to the attestation. */ export async function attest(options: AttestOptions): Promise { - const subject: Subject = { - name: options.subjectName, - digest: options.subjectDigest + let subjects: Subject[] + + if (options.subjects) { + subjects = options.subjects + } else if (options.subjectName && options.subjectDigest) { + subjects = [{name: options.subjectName, digest: options.subjectDigest}] + } else { + throw new Error( + 'Must provide either subjectName and subjectDigest or subjects' + ) } + const predicate: Predicate = { type: options.predicateType, params: options.predicate } - const statement = buildIntotoStatement(subject, predicate) + + const statement = buildIntotoStatement(subjects, predicate) // Sign the provenance statement const payload: Payload = { diff --git a/packages/attest/src/intoto.ts b/packages/attest/src/intoto.ts index 9d6a2d0e..5a2dcc9f 100644 --- a/packages/attest/src/intoto.ts +++ b/packages/attest/src/intoto.ts @@ -20,12 +20,12 @@ export type InTotoStatement = { * @returns The constructed in-toto statement. */ export const buildIntotoStatement = ( - subject: Subject, + subjects: Subject[], predicate: Predicate ): InTotoStatement => { return { _type: INTOTO_STATEMENT_V1_TYPE, - subject: [subject], + subject: subjects, predicateType: predicate.type, predicate: predicate.params }