Skip to content
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

Support single-subject attestations #219

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -138,6 +138,11 @@ Attestations are saved in the JSON-serialized [Sigstore bundle][6] format.
If multiple subjects are being attested at the same time, a single attestation
will be created with references to each of the supplied subjects.

If the `single-subject-attestations` option has been set to true,
one attestation will be generated per provided subject.
All of these attestations will be written to the output file,
using the [JSON Lines][7] format (one attestation per line).

## Attestation Limits

### Subject Limits
@@ -320,6 +325,7 @@ jobs:
[5]: https://cli.github.com/manual/gh_attestation_verify
[6]:
https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto
[7]: https://jsonlines.org/
[8]: https://github.com/actions/toolkit/tree/main/packages/glob#patterns
[9]:
https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
1 change: 1 addition & 0 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -47,6 +47,7 @@ const defaultInputs: main.RunInputs = {
pushToRegistry: false,
showSummary: true,
githubToken: '',
singleSubjectAttestations: false,
privateSigning: false
}

5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -64,6 +64,11 @@ inputs:
The GitHub token used to make authenticated API requests.
default: ${{ github.token }}
required: false
single-subject-attestations:
description: >
If true, generate one attestation per subject. Defaults to false.
default: false
required: false
outputs:
bundle-path:
description: 'The path to the file containing the attestation bundle.'
2 changes: 1 addition & 1 deletion src/attest.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export const createAttestation = async (
}
): Promise<AttestResult> => {
// Sign provenance w/ Sigstore
const attestation = await attest({
const attestation: Attestation = await attest({
subjects,
predicateType: predicate.type,
predicate: predicate.params,
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,9 @@ const inputs: RunInputs = {
pushToRegistry: core.getBooleanInput('push-to-registry'),
showSummary: core.getBooleanInput('show-summary'),
githubToken: core.getInput('github-token'),
singleSubjectAttestations: core.getBooleanInput(
'single-subject-attestations'
),
// undocumented -- not part of public interface
privateSigning: ['true', 'True', 'TRUE', '1'].includes(
core.getInput('private-signing')
85 changes: 56 additions & 29 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@
pushToRegistry: boolean
githubToken: string
showSummary: boolean
singleSubjectAttestations: boolean
// undocumented -- not part of public interface
privateSigning: boolean
}

@@ -65,27 +67,43 @@
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
core.setOutput('bundle-path', outputPath)

const att = await createAttestation(subjects, predicate, {
const opts = {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
githubToken: inputs.githubToken
})
}

logAttestation(subjects, att, sigstoreInstance)
const atts: AttestResult[] = []
if (inputs.singleSubjectAttestations) {
// Generate one attestation for each subject
for (const subject of subjects) {
const att = await createAttestation([subject], predicate, opts)
atts.push(att)
}
} else {
const att = await createAttestation(subjects, predicate, opts)
atts.push(att)
}

// Write attestation bundle to output file
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
encoding: 'utf-8',
flag: 'a'
})
for (const att of atts) {
logAttestation(att, sigstoreInstance)

// Write attestation bundle to output file
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
encoding: 'utf-8',
flag: 'a'
})
}

logSubjects(subjects)

if (att.attestationID) {
core.setOutput('attestation-id', att.attestationID)
core.setOutput('attestation-url', attestationURL(att.attestationID))
if (atts[0].attestationID) {
core.setOutput('attestation-id', atts[0].attestationID)
core.setOutput('attestation-url', attestationURL(atts[0].attestationID))
}

if (inputs.showSummary) {
await logSummary(att)
await logSummary(atts)
}
} catch (err) {
// Fail the workflow run if an error occurs
@@ -110,18 +128,9 @@

// Log details about the attestation to the GitHub Actions run
const logAttestation = (
subjects: Subject[],
attestation: AttestResult,
sigstoreInstance: SigstoreInstance
): void => {
if (subjects.length === 1) {
core.info(
`Attestation created for ${subjects[0].name}@${formatSubjectDigest(subjects[0])}`
)
} else {
core.info(`Attestation created for ${subjects.length} subjects`)
}

const instanceName =
sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'
core.startGroup(
@@ -148,20 +157,38 @@

if (attestation.attestationDigest) {
core.info(style.highlight('Attestation uploaded to registry'))
core.info(`${subjects[0].name}@${attestation.attestationDigest}`)

Check failure on line 160 in src/main.ts

GitHub Actions / TypeScript Tests

Unsafe member access [0] on an `error` typed value
}
}

// Log details about attestation subjects to the GitHub Actions run
const logSubjects = (subjects: Subject[]): void => {
core.info(`Attestation created for ${subjects.length} subjects`)
for (const subject of subjects) {
core.info(
`Attestation created for ${subject.name}@${formatSubjectDigest(subject)}`
)
}
}

// Attach summary information to the GitHub Actions run
const logSummary = async (attestation: AttestResult): Promise<void> => {
const { attestationID } = attestation

if (attestationID) {
const url = attestationURL(attestationID)
core.summary.addHeading('Attestation Created', 3)
core.summary.addList([`<a href="${url}">${url}</a>`])
await core.summary.write()
const logSummary = async (attestations: AttestResult[]): Promise<void> => {
if (attestations.length <= 0) return

core.summary.addHeading(
/* istanbul ignore next */
attestations.length !== 1 ? 'Attestations Created' : 'Attestation Created',
3
)
const listItems: string[] = []
for (const { attestationID } of attestations) {
if (attestationID) {
const url = attestationURL(attestationID)
listItems.push(`<a href="${url}">${url}</a>`)
}
}
core.summary.addList(listItems)
await core.summary.write()
}

const tempDir = (): string => {
1 change: 1 addition & 0 deletions src/subject.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ export type SubjectInputs = {
subjectName: string
subjectDigest: string
subjectChecksums: string
singleSubjectAttestations: boolean
downcaseName?: boolean
}
// Returns the subject specified by the action's inputs. The subject may be
Loading
Oops, something went wrong.