Skip to content

Commit

Permalink
Merge pull request #1667 from actions/bdehamer/attest
Browse files Browse the repository at this point in the history
add new @actions/attest package
  • Loading branch information
bdehamer committed Feb 26, 2024
2 parents 437f2be + 6079dea commit ad1f156
Show file tree
Hide file tree
Showing 23 changed files with 4,221 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

/packages/artifact/ @actions/artifacts-actions
/packages/cache/ @actions/actions-cache
/package/attest/ @actions/package-security
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ $ npm install @actions/cache
```
<br/>

:lock_with_ink_pen: [@actions/attest](packages/attest)

Provides functions to write attestations for workflow artifacts. Read more [here](packages/attest)

```bash
$ npm install @actions/attest
```
<br/>

## Creating an Action with the Toolkit

:question: [Choosing an action type](docs/action-types.md)
Expand Down
9 changes: 9 additions & 0 deletions packages/attest/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright 2024 GitHub

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
172 changes: 172 additions & 0 deletions packages/attest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# `@actions/attest`

Functions for generating signed attestations for workflow artifacts.

Attestations bind some subject (a named artifact along with its digest) to a
predicate (some assertion about that subject) using the [in-toto
statement](https://github.com/in-toto/attestation/tree/main/spec/v1) format. A
signature is generated for the attestation using a
[Sigstore](https://www.sigstore.dev/)-issued signing certificate.

Once the attestation has been created and signed, it will be uploaded to the GH
attestations API and associated with the repository from which the workflow was
initiated.

## Usage

### `attest`

The `attest` function takes the supplied subject/predicate pair and generates a
signed attestation.

```js
const { attest } = require('@actions/attest');
const core = require('@actions/core');

async function run() {
// In order to persist attestations to the repo, this should be a token with
// repository write permissions.
const ghToken = core.getInput('gh-token');

const attestation = await attest({
subjectName: 'my-artifact-name',
subjectDigest: { 'sha256': '36ab4667...'},
predicateType: 'https://in-toto.io/attestation/release',
predicate: { . . . },
token: ghToken
});

console.log(attestation);
}

run();
```

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<string, string>
// URI identifying the content type of the predicate being attested.
predicateType: string
// Predicate to be attested.
predicate: object
// GitHub token for writing attestations.
token: string
// Sigstore instance to use for signing. Must be one of "public-good" or
// "github".
sigstore?: 'public-good' | 'github'
// Whether to skip writing the attestation to the GH attestations API.
skipWrite?: boolean
}
```
### `attestProvenance`
The `attestProvenance` function accepts the name and digest of some artifact and
generates a build provenance attestation over those values.
The attestation is formed by first generating a [SLSA provenance
predicate](https://slsa.dev/spec/v1.0/provenance) populated with
[metadata](https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1)
pulled from the GitHub Actions run.
```js
const { attestProvenance } = require('@actions/attest');
const core = require('@actions/core');

async function run() {
// In order to persist attestations to the repo, this should be a token with
// repository write permissions.
const ghToken = core.getInput('gh-token');

const attestation = await attestProvenance({
subjectName: 'my-artifact-name',
subjectDigest: { 'sha256': '36ab4667...'},
token: ghToken
});

console.log(attestation);
}

run();
```

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<string, string>
// GitHub token for writing attestations.
token: string
// Sigstore instance to use for signing. Must be one of "public-good" or
// "github".
sigstore?: 'public-good' | 'github'
// Whether to skip writing the attestation to the GH attestations API.
skipWrite?: boolean
}
```
### `Attestation`
The `Attestation` returned by `attest`/`attestProvenance` has the following
fields:
```typescript
export type Attestation = {
/*
* JSON-serialized Sigstore bundle containing the provenance attestation,
* signature, signing certificate and witnessed timestamp.
*/
bundle: SerializedBundle
/*
* PEM-encoded signing certificate used to sign the attestation.
*/
certificate: string
/*
* ID of Rekor transparency log entry created for the attestation (if
* applicable).
*/
tlogID?: string
/*
* ID of the persisted attestation (accessible via the GH API).
*/
attestationID?: string
}
```
For details about the Sigstore bundle format, see the [Bundle protobuf
specification](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto).
## Sigstore Instance
When generating the signed attestation there are two different Sigstore
instances which can be used to issue the signing certificate. By default,
workflows initiated from public repositories will use the Sigstore public-good
instance and persist the attestation signature to the public [Rekor transparency
log](https://docs.sigstore.dev/logging/overview/). Workflows initiated from
private/internal repositories will use the GitHub-internal Sigstore instance
which uses a signed timestamp issued by GitHub's timestamp authority in place of
the public transparency log.
The default Sigstore instance selection can be overridden by passing an explicit
value of either "public-good" or "github" for the `sigstore` option when calling
either `attest` or `attestProvenance`.
## Storage
Attestations created by `attest`/`attestProvenance` will be uploaded to the GH
attestations API and associated with the appropriate repository. Attestation
storage is only supported for public repositories or repositories which belong
to a GitHub Enterprise Cloud account.
In order to generate attestations for private, non-Enterprise repositories, the
`skipWrite` option should be set to `true`.
5 changes: 5 additions & 0 deletions packages/attest/RELEASES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @actions/attest Releases

### 1.0.0

- Initial release
19 changes: 19 additions & 0 deletions packages/attest/__tests__/__snapshots__/intoto.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`buildIntotoStatement returns a provenance hydrated from env vars 1`] = `
{
"_type": "https://in-toto.io/Statement/v1",
"predicate": {
"key": "value",
},
"predicateType": "predicatey",
"subject": [
{
"digest": {
"sha256": "7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32",
},
"name": "subjecty",
},
],
}
`;
42 changes: 42 additions & 0 deletions packages/attest/__tests__/__snapshots__/provenance.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`buildSLSAProvenancePredicate returns a provenance hydrated from env vars 1`] = `
{
"params": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"path": ".github/workflows/main.yml",
"ref": "main",
"repository": "https://github.com/owner/repo",
},
},
"internalParameters": {
"github": {
"event_name": "push",
"repository_id": "repo-id",
"repository_owner_id": "owner-id",
},
},
"resolvedDependencies": [
{
"digest": {
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
},
"uri": "git+https://github.com/owner/repo@refs/heads/main",
},
],
},
"runDetails": {
"builder": {
"id": "https://github.com/actions/runner/github-hosted",
},
"metadata": {
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
},
},
},
"type": "https://slsa.dev/provenance/v1",
}
`;
6 changes: 6 additions & 0 deletions packages/attest/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {attest, attestProvenance} from '../src'

it('exports functions', () => {
expect(attestProvenance).toBeInstanceOf(Function)
expect(attest).toBeInstanceOf(Function)
})
23 changes: 23 additions & 0 deletions packages/attest/__tests__/intoto.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {buildIntotoStatement} from '../src/intoto'
import type {Predicate, Subject} from '../src/shared.types'

describe('buildIntotoStatement', () => {
const subject: Subject = {
name: 'subjecty',
digest: {
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
}

const predicate: Predicate = {
type: 'predicatey',
params: {
key: 'value'
}
}

it('returns a provenance hydrated from env vars', () => {
const statement = buildIntotoStatement(subject, predicate)
expect(statement).toMatchSnapshot()
})
})
Loading

0 comments on commit ad1f156

Please sign in to comment.