Skip to content

Commit 2be0db0

Browse files
authored
feat: Input argument for the name of attestation of generic workflow (slsa-framework#545)
* Input argument for the name of attestation * update * update * tests * update * update * update * update * update * update
1 parent aacb56f commit 2be0db0

File tree

3 files changed

+184
-13
lines changed

3 files changed

+184
-13
lines changed

.github/workflows/generator_generic_slsa3.yml

+21-13
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ on:
3434
required: false
3535
type: boolean
3636
default: false
37+
attestation-name:
38+
description: >
39+
The artifact name of the signed provenance.
40+
The file must have the intoto.jsonl extension.
41+
42+
Default: attestation.intoto.jsonl
43+
required: false
44+
type: string
45+
default: "attestation.intoto.jsonl"
3746
compile-generator:
3847
description: "Build the generator from source. This increases build time by ~2m."
3948
required: false
@@ -44,8 +53,8 @@ on:
4453
description: "The name of the release where provenance was uploaded."
4554
value: ${{ jobs.create-release.outputs.release-id }}
4655
attestation-name:
47-
description: "The artifact name of the signed provenance"
48-
value: ${{ jobs.generator.outputs.attestation-name }}
56+
description: "The artifact name of the signed provenance. (A file with the intoto.jsonl extension)."
57+
value: "${{ inputs.attestation-name }}"
4958

5059
jobs:
5160
# detect-env detects the reusable workflow's repository and ref for use later
@@ -73,7 +82,6 @@ jobs:
7382
# reference.
7483
generator:
7584
outputs:
76-
attestation-name: ${{ steps.sign-prov.outputs.attestation-name }}
7785
attestation-sha256: ${{ steps.sign-prov.outputs.attestation-sha256 }}
7886
runs-on: ubuntu-latest
7987
needs: [detect-env]
@@ -103,21 +111,21 @@ jobs:
103111
env:
104112
SUBJECTS: "${{ inputs.base64-subjects }}"
105113
GITHUB_CONTEXT: "${{ toJSON(github) }}"
114+
UNTRUSTED_ATTESTATION_NAME: "${{ inputs.attestation-name }}"
106115
run: |
107116
set -euo pipefail
108-
# Create and sign provenance
109-
# This sets attestation-name to the name of the signed DSSE envelope.
110-
attestation_name="attestation.intoto.jsonl"
111-
./"$BUILDER_BINARY" attest --subjects "${SUBJECTS}" -g $attestation_name
112-
attestation_sha256=$(sha256sum $attestation_name | awk '{print $1}')
113-
echo "::set-output name=attestation-name::$attestation_name"
117+
# Create and sign provenance.
118+
# Note: The builder verifies that the UNTRUSTED_ATTESTATION_NAME is located
119+
# in the current directory.
120+
./"$BUILDER_BINARY" attest --subjects "${SUBJECTS}" -g "$UNTRUSTED_ATTESTATION_NAME"
121+
attestation_sha256=$(sha256sum "$UNTRUSTED_ATTESTATION_NAME" | awk '{print $1}')
114122
echo "::set-output name=attestation-sha256::$attestation_sha256"
115123
116124
- name: Upload the signed provenance
117125
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0
118126
with:
119-
name: "${{ steps.sign-prov.outputs.attestation-name }}"
120-
path: "${{ steps.sign-prov.outputs.attestation-name }}"
127+
name: "${{ inputs.attestation-name }}"
128+
path: "${{ inputs.attestation-name }}"
121129
if-no-files-found: error
122130
retention-days: 5
123131

@@ -135,12 +143,12 @@ jobs:
135143
- name: Download the provenance
136144
uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@1d646d70aeba1516af69fb0ef48206580122449b
137145
with:
138-
name: "${{ needs.generator.outputs.attestation-name }}"
146+
name: "${{ inputs.attestation-name }}"
139147
sha256: "${{ needs.generator.outputs.attestation-sha256 }}"
140148

141149
- name: Release
142150
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # tag=v0.1.14
143151
id: release
144152
with:
145153
files: |
146-
${{ needs.generator.outputs.attestation-name }}
154+
${{ inputs.attestation-name }}

internal/builders/generic/attest.go

+48
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ type errNoName struct {
6464
errors.WrappableError
6565
}
6666

67+
// errInvalidPath indicates an invalid path.
68+
type errInvalidPath struct {
69+
errors.WrappableError
70+
}
71+
72+
// errInternal indicates an internal error.
73+
type errInternal struct {
74+
errors.WrappableError
75+
}
76+
6777
// errDuplicateSubject indicates a duplicate subject name.
6878
type errDuplicateSubject struct {
6979
errors.WrappableError
@@ -125,13 +135,46 @@ func parseSubjects(b64str string) ([]intoto.Subject, error) {
125135
return parsed, nil
126136
}
127137

138+
func pathIsUnderCurrentDirectory(path string) error {
139+
wd, err := os.Getwd()
140+
if err != nil {
141+
return errors.Errorf(&errInternal{}, "os.Getwd(): %w", err)
142+
}
143+
p, err := filepath.Abs(path)
144+
if err != nil {
145+
return errors.Errorf(&errInternal{}, "filepath.Abs(): %w", err)
146+
}
147+
148+
if !strings.HasPrefix(p, wd+"/") &&
149+
wd != p {
150+
return errors.Errorf(&errInvalidPath{}, "path: %q", path)
151+
}
152+
153+
return nil
154+
}
155+
128156
func getFile(path string) (io.Writer, error) {
129157
if path == "-" {
130158
return os.Stdout, nil
131159
}
160+
161+
if err := pathIsUnderCurrentDirectory(path); err != nil {
162+
return nil, err
163+
}
164+
132165
return os.OpenFile(filepath.Clean(path), os.O_WRONLY|os.O_CREATE, 0o600)
133166
}
134167

168+
func verifyAttestationPath(path string) error {
169+
if !strings.HasSuffix(path, "intoto.jsonl") {
170+
return errors.Errorf(&errInvalidPath{}, "invalid suffix: %q. Must be .intoto.jsonl", path)
171+
}
172+
if err := pathIsUnderCurrentDirectory(path); err != nil {
173+
return err
174+
}
175+
return nil
176+
}
177+
135178
type provenanceOnlyBuild struct {
136179
*slsa.GithubActionsBuild
137180
}
@@ -158,6 +201,10 @@ run in the context of a Github Actions workflow.`,
158201
ghContext, err := github.GetWorkflowContext()
159202
check(err)
160203

204+
// Verify the extension path and extension.
205+
err = verifyAttestationPath(attPath)
206+
check(err)
207+
161208
var parsedSubjects []intoto.Subject
162209
// We don't actually care about the subjects if we aren't writing an attestation.
163210
if attPath != "" {
@@ -189,6 +236,7 @@ run in the context of a Github Actions workflow.`,
189236
p, err := g.Generate(ctx)
190237
check(err)
191238

239+
// Note: we verify the path within getFile().
192240
if attPath != "" {
193241
var attBytes []byte
194242
if utils.IsPresubmitTests() {

internal/builders/generic/attest_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,124 @@ import (
77
"github.com/google/go-cmp/cmp/cmpopts"
88
intoto "github.com/in-toto/in-toto-golang/in_toto"
99
slsav02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
10+
1011
"github.com/slsa-framework/slsa-github-generator/internal/errors"
1112
)
1213

14+
func Test_pathIsUnderCurrentDirectory(t *testing.T) {
15+
t.Parallel()
16+
17+
tests := []struct {
18+
name string
19+
path string
20+
expected error
21+
}{
22+
{
23+
name: "valid same path",
24+
path: "./",
25+
expected: nil,
26+
},
27+
{
28+
name: "valid path no slash",
29+
path: "./some/valid/path",
30+
expected: nil,
31+
},
32+
{
33+
name: "valid path with slash",
34+
path: "./some/valid/path/",
35+
expected: nil,
36+
},
37+
{
38+
name: "valid path with no dot",
39+
path: "some/valid/path/",
40+
expected: nil,
41+
},
42+
{
43+
name: "some valid path",
44+
path: "../generic/some/valid/path",
45+
expected: nil,
46+
},
47+
{
48+
name: "parent invalid path",
49+
path: "../invalid/path",
50+
expected: &errInvalidPath{},
51+
},
52+
{
53+
name: "some invalid fullpath",
54+
path: "/some/invalid/fullpath",
55+
expected: &errInvalidPath{},
56+
},
57+
}
58+
for _, tt := range tests {
59+
tt := tt // Re-initializing variable so it is not changed while executing the closure below
60+
t.Run(tt.name, func(t *testing.T) {
61+
t.Parallel()
62+
63+
err := pathIsUnderCurrentDirectory(tt.path)
64+
if (err == nil && tt.expected != nil) ||
65+
(err != nil && tt.expected == nil) {
66+
t.Fatalf("unexpected error: %v", cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
67+
}
68+
69+
if err != nil && !errors.As(err, &tt.expected) {
70+
t.Fatalf("unexpected error: %v", cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
71+
}
72+
})
73+
}
74+
}
75+
76+
func Test_verifyAttestationPath(t *testing.T) {
77+
t.Parallel()
78+
79+
tests := []struct {
80+
name string
81+
path string
82+
expected error
83+
}{
84+
{
85+
name: "valid file",
86+
path: "./path/to/valid.intoto.jsonl",
87+
expected: nil,
88+
},
89+
{
90+
name: "invalid path",
91+
path: "../some/invalid/valid.intoto.jsonl",
92+
expected: &errInvalidPath{},
93+
},
94+
{
95+
name: "invalid extension",
96+
path: "some/file.ntoto.jsonl",
97+
expected: &errInvalidPath{},
98+
},
99+
{
100+
name: "invalid not exntension",
101+
path: "some/file.intoto.jsonl.",
102+
expected: &errInvalidPath{},
103+
},
104+
{
105+
name: "invalid folder exntension",
106+
path: "file.intoto.jsonl/file",
107+
expected: &errInvalidPath{},
108+
},
109+
}
110+
for _, tt := range tests {
111+
tt := tt // Re-initializing variable so it is not changed while executing the closure below
112+
t.Run(tt.name, func(t *testing.T) {
113+
t.Parallel()
114+
115+
err := verifyAttestationPath(tt.path)
116+
if (err == nil && tt.expected != nil) ||
117+
(err != nil && tt.expected == nil) {
118+
t.Fatalf("unexpected error: %v", cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
119+
}
120+
121+
if err != nil && !errors.As(err, &tt.expected) {
122+
t.Fatalf("unexpected error: %v", cmp.Diff(err, tt.expected, cmpopts.EquateErrors()))
123+
}
124+
})
125+
}
126+
}
127+
13128
// TestParseSubjects tests the parseSubjects function.
14129
func TestParseSubjects(t *testing.T) {
15130
testCases := []struct {

0 commit comments

Comments
 (0)