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

New subject-checksums input param #198

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
54 changes: 48 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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: '{}'
```

<!-- markdownlint-disable MD038 -->

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.

<!-- markdownlint-enable MD038 -->

```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
Expand Down
5 changes: 4 additions & 1 deletion __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const defaultInputs: main.RunInputs = {
subjectName: '',
subjectDigest: '',
subjectPath: '',
subjectChecksums: '',
pushToRegistry: false,
showSummary: true,
githubToken: '',
Expand Down Expand Up @@ -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'
)
)
})
})
Expand Down
138 changes: 134 additions & 4 deletions __tests__/subject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
})
})
Expand All @@ -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
)
})
})
Expand Down Expand Up @@ -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', () => {
Expand Down
18 changes: 12 additions & 6 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >
Expand Down
Loading
Loading