Skip to content

Commit 6079dea

Browse files
committed
add new @actions/attest package
Signed-off-by: Brian DeHamer <[email protected]>
1 parent 415c42d commit 6079dea

24 files changed

+4224
-3
lines changed

CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
/packages/artifact/ @actions/artifacts-actions
44
/packages/cache/ @actions/actions-cache
5+
/package/attest/ @actions/package-security

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ $ npm install @actions/cache
102102
```
103103
<br/>
104104

105+
:lock_with_ink_pen: [@actions/attest](packages/attest)
106+
107+
Provides functions to write attestations for workflow artifacts. Read more [here](packages/attest)
108+
109+
```bash
110+
$ npm install @actions/attest
111+
```
112+
<br/>
113+
105114
## Creating an Action with the Toolkit
106115

107116
:question: [Choosing an action type](docs/action-types.md)

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/attest/LICENSE.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The MIT License (MIT)
2+
3+
Copyright 2024 GitHub
4+
5+
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:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
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.

packages/attest/README.md

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# `@actions/attest`
2+
3+
Functions for generating signed attestations for workflow artifacts.
4+
5+
Attestations bind some subject (a named artifact along with its digest) to a
6+
predicate (some assertion about that subject) using the [in-toto
7+
statement](https://github.com/in-toto/attestation/tree/main/spec/v1) format. A
8+
signature is generated for the attestation using a
9+
[Sigstore](https://www.sigstore.dev/)-issued signing certificate.
10+
11+
Once the attestation has been created and signed, it will be uploaded to the GH
12+
attestations API and associated with the repository from which the workflow was
13+
initiated.
14+
15+
## Usage
16+
17+
### `attest`
18+
19+
The `attest` function takes the supplied subject/predicate pair and generates a
20+
signed attestation.
21+
22+
```js
23+
const { attest } = require('@actions/attest');
24+
const core = require('@actions/core');
25+
26+
async function run() {
27+
// In order to persist attestations to the repo, this should be a token with
28+
// repository write permissions.
29+
const ghToken = core.getInput('gh-token');
30+
31+
const attestation = await attest({
32+
subjectName: 'my-artifact-name',
33+
subjectDigest: { 'sha256': '36ab4667...'},
34+
predicateType: 'https://in-toto.io/attestation/release',
35+
predicate: { . . . },
36+
token: ghToken
37+
});
38+
39+
console.log(attestation);
40+
}
41+
42+
run();
43+
```
44+
45+
The `attest` function supports the following options:
46+
47+
```typescript
48+
export type AttestOptions = {
49+
// The name of the subject to be attested.
50+
subjectName: string
51+
// The digest of the subject to be attested. Should be a map of digest
52+
// algorithms to their hex-encoded values.
53+
subjectDigest: Record<string, string>
54+
// URI identifying the content type of the predicate being attested.
55+
predicateType: string
56+
// Predicate to be attested.
57+
predicate: object
58+
// GitHub token for writing attestations.
59+
token: string
60+
// Sigstore instance to use for signing. Must be one of "public-good" or
61+
// "github".
62+
sigstore?: 'public-good' | 'github'
63+
// Whether to skip writing the attestation to the GH attestations API.
64+
skipWrite?: boolean
65+
}
66+
```
67+
68+
### `attestProvenance`
69+
70+
The `attestProvenance` function accepts the name and digest of some artifact and
71+
generates a build provenance attestation over those values.
72+
73+
The attestation is formed by first generating a [SLSA provenance
74+
predicate](https://slsa.dev/spec/v1.0/provenance) populated with
75+
[metadata](https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1)
76+
pulled from the GitHub Actions run.
77+
78+
```js
79+
const { attestProvenance } = require('@actions/attest');
80+
const core = require('@actions/core');
81+
82+
async function run() {
83+
// In order to persist attestations to the repo, this should be a token with
84+
// repository write permissions.
85+
const ghToken = core.getInput('gh-token');
86+
87+
const attestation = await attestProvenance({
88+
subjectName: 'my-artifact-name',
89+
subjectDigest: { 'sha256': '36ab4667...'},
90+
token: ghToken
91+
});
92+
93+
console.log(attestation);
94+
}
95+
96+
run();
97+
```
98+
99+
The `attestProvenance` function supports the following options:
100+
101+
```typescript
102+
export type AttestProvenanceOptions = {
103+
// The name of the subject to be attested.
104+
subjectName: string
105+
// The digest of the subject to be attested. Should be a map of digest
106+
// algorithms to their hex-encoded values.
107+
subjectDigest: Record<string, string>
108+
// GitHub token for writing attestations.
109+
token: string
110+
// Sigstore instance to use for signing. Must be one of "public-good" or
111+
// "github".
112+
sigstore?: 'public-good' | 'github'
113+
// Whether to skip writing the attestation to the GH attestations API.
114+
skipWrite?: boolean
115+
}
116+
```
117+
118+
### `Attestation`
119+
120+
The `Attestation` returned by `attest`/`attestProvenance` has the following
121+
fields:
122+
123+
```typescript
124+
export type Attestation = {
125+
/*
126+
* JSON-serialized Sigstore bundle containing the provenance attestation,
127+
* signature, signing certificate and witnessed timestamp.
128+
*/
129+
bundle: SerializedBundle
130+
/*
131+
* PEM-encoded signing certificate used to sign the attestation.
132+
*/
133+
certificate: string
134+
/*
135+
* ID of Rekor transparency log entry created for the attestation (if
136+
* applicable).
137+
*/
138+
tlogID?: string
139+
/*
140+
* ID of the persisted attestation (accessible via the GH API).
141+
*/
142+
attestationID?: string
143+
}
144+
```
145+
146+
For details about the Sigstore bundle format, see the [Bundle protobuf
147+
specification](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto).
148+
149+
## Sigstore Instance
150+
151+
When generating the signed attestation there are two different Sigstore
152+
instances which can be used to issue the signing certificate. By default,
153+
workflows initiated from public repositories will use the Sigstore public-good
154+
instance and persist the attestation signature to the public [Rekor transparency
155+
log](https://docs.sigstore.dev/logging/overview/). Workflows initiated from
156+
private/internal repositories will use the GitHub-internal Sigstore instance
157+
which uses a signed timestamp issued by GitHub's timestamp authority in place of
158+
the public transparency log.
159+
160+
The default Sigstore instance selection can be overridden by passing an explicit
161+
value of either "public-good" or "github" for the `sigstore` option when calling
162+
either `attest` or `attestProvenance`.
163+
164+
## Storage
165+
166+
Attestations created by `attest`/`attestProvenance` will be uploaded to the GH
167+
attestations API and associated with the appropriate repository. Attestation
168+
storage is only supported for public repositories or repositories which belong
169+
to a GitHub Enterprise Cloud account.
170+
171+
In order to generate attestations for private, non-Enterprise repositories, the
172+
`skipWrite` option should be set to `true`.

packages/attest/RELEASES.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @actions/attest Releases
2+
3+
### 1.0.0
4+
5+
- Initial release
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`buildIntotoStatement returns a provenance hydrated from env vars 1`] = `
4+
{
5+
"_type": "https://in-toto.io/Statement/v1",
6+
"predicate": {
7+
"key": "value",
8+
},
9+
"predicateType": "predicatey",
10+
"subject": [
11+
{
12+
"digest": {
13+
"sha256": "7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32",
14+
},
15+
"name": "subjecty",
16+
},
17+
],
18+
}
19+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`buildSLSAProvenancePredicate returns a provenance hydrated from env vars 1`] = `
4+
{
5+
"params": {
6+
"buildDefinition": {
7+
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
8+
"externalParameters": {
9+
"workflow": {
10+
"path": ".github/workflows/main.yml",
11+
"ref": "main",
12+
"repository": "https://github.com/owner/repo",
13+
},
14+
},
15+
"internalParameters": {
16+
"github": {
17+
"event_name": "push",
18+
"repository_id": "repo-id",
19+
"repository_owner_id": "owner-id",
20+
},
21+
},
22+
"resolvedDependencies": [
23+
{
24+
"digest": {
25+
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
26+
},
27+
"uri": "git+https://github.com/owner/repo@refs/heads/main",
28+
},
29+
],
30+
},
31+
"runDetails": {
32+
"builder": {
33+
"id": "https://github.com/actions/runner/github-hosted",
34+
},
35+
"metadata": {
36+
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
37+
},
38+
},
39+
},
40+
"type": "https://slsa.dev/provenance/v1",
41+
}
42+
`;
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {attest, attestProvenance} from '../src'
2+
3+
it('exports functions', () => {
4+
expect(attestProvenance).toBeInstanceOf(Function)
5+
expect(attest).toBeInstanceOf(Function)
6+
})
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {buildIntotoStatement} from '../src/intoto'
2+
import type {Predicate, Subject} from '../src/shared.types'
3+
4+
describe('buildIntotoStatement', () => {
5+
const subject: Subject = {
6+
name: 'subjecty',
7+
digest: {
8+
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
9+
}
10+
}
11+
12+
const predicate: Predicate = {
13+
type: 'predicatey',
14+
params: {
15+
key: 'value'
16+
}
17+
}
18+
19+
it('returns a provenance hydrated from env vars', () => {
20+
const statement = buildIntotoStatement(subject, predicate)
21+
expect(statement).toMatchSnapshot()
22+
})
23+
})

0 commit comments

Comments
 (0)