Skip to content

Commit 096c709

Browse files
authored
Change format of subjects to base64 (#174)
* Use base64 for subjects. * Update docs * Update tests for parseSubject * Update docs for subjects input * Change input to base64-subjects * Add comment on sha256sum
1 parent 49176b3 commit 096c709

File tree

5 files changed

+104
-50
lines changed

5 files changed

+104
-50
lines changed

.github/workflows/pre-submit.e2e.generic.workflow.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ jobs:
1414
actions: read # For reading workflow info.
1515
uses: ./.github/workflows/slsa2_provenance.yml
1616
with:
17-
subjects: "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 binary-name"
17+
# echo "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 binary-name" | base64 -w0
18+
subjects: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiAgICBiaW5hcnktbmFtZQo="
1819

1920
verify:
2021
runs-on: ubuntu-latest

.github/workflows/slsa2_provenance.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ permissions:
2525
on:
2626
workflow_call:
2727
inputs:
28-
subjects:
29-
description: "Artifacts for which to generate provenance, formatted the same as the output of sha256sum (SHA256 NAME\\n[...])"
28+
base64-subjects:
29+
description: "Artifacts for which to generate provenance, formatted the same as the output of sha256sum (SHA256 NAME\\n[...]) and base64 encoded."
3030
required: true
3131
type: string
3232
outputs:
@@ -104,7 +104,7 @@ jobs:
104104
# order to avoid script injection.
105105
# See: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
106106
env:
107-
SUBJECTS: "${{ inputs.subjects }}"
107+
SUBJECTS: "${{ inputs.base64-subjects }}"
108108
GITHUB_CONTEXT: "${{ toJSON(github) }}"
109109
run: |
110110
set -euo pipefail

internal/builders/generic/README.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,20 @@ To get started, you will need to add some steps to your current workflow. We
3030
will assume you have an existing Github Actions workflow to build your project.
3131

3232
Add a step to your workflow after you have built your project to generate a
33-
sha256 hash of your artifacts. The following assumes you have a binary called
34-
`binary-linux-amd64`.
33+
sha256 hash of your artifacts and base64 encode it.
3534

36-
After that, add a new job to call the `slsa-github-generator` reusable workflow.
35+
Assuming you have a binary called `binary-linux-amd64` you can use the
36+
`sha256sum` and `base64` commands to create the digest. Here we use the `-w0` to
37+
output the encoded data on one line and make it easier to use as a Github Actions
38+
output:
39+
40+
```shell
41+
$ sha256sum artifact1 artifact2 ... | base64 -w0
42+
```
43+
44+
After you have encoded your digest, add a new job to call the
45+
`slsa-github-generator` reusable workflow. Here's an example of what it might
46+
look like all together.
3747

3848
```yaml
3949
jobs:
@@ -42,25 +52,26 @@ jobs:
4252
digest: ${{ steps.hash.outputs.digest }}
4353
runs-on: ubuntu-latest
4454
steps:
45-
# Your build steps are here.
55+
- name: "build artifacts"
56+
run: |
57+
# Build build artifacts here.
4658
- name: "generate hash"
4759
shell: bash
4860
id: hash
4961
run: |
5062
set -euo pipefail
51-
DIGEST=$(sha256sum binary-linux-amd64)
52-
DIGEST="${DIGEST//'%'/'%25'}"
53-
DIGEST="${DIGEST//$'\n'/'%0A'}"
54-
DIGEST="${DIGEST//$'\r'/'%0D'}"
55-
echo "::set-output name=digest::$DIGEST"
63+
# sha256sum generates sha256 hash for all artifacts.
64+
# base64 -w0 encodes to base64 and outputs on a single line.
65+
# sha256sum artifact1 artifact2 ... | base64 -w0
66+
echo "::set-output name=digest::$(sha256sum artifact1 artifact2 | base64 -w0)"
5667
provenance:
5768
needs: [build]
5869
permissions:
5970
id-token: write
6071
contents: read
6172
uses: slsa-framework/slsa-github-generator/.github/workflows/slsa2_provenance.yml@main
6273
with:
63-
subjects: "${{ needs.build.outputs.digest }}"
74+
base64-subjects: "${{ needs.build.outputs.digest }}"
6475
```
6576
6677
### Workflow Inputs
@@ -69,9 +80,11 @@ The builder workflow
6980
[.github/workflows/slsa2_provenance.yml](.github/workflows/slsa2_provenance.yml) accepts
7081
the following inputs:
7182
72-
| Name | Required | Description |
73-
| ---------- | -------- | -------------------------------------------------------------------------------------------------------------- |
74-
| `subjects` | yes | Artifacts for which to generate provenance, formatted the same as the output of sha256sum (SHA256 NAME\n[...]) |
83+
| Name | Required | Description |
84+
| ----------------- | -------- | ----------------------------------------------------- |
85+
| `base64-subjects` | yes | Artifacts for which to generate provenance, formatted |
86+
| | | the same as the output of sha256sum |
87+
| | | (SHA256 NAME\n[...]) and base64 encoded. |
7588

7689
### Workflow Outputs
7790

internal/builders/generic/attest.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ package main
1616

1717
import (
1818
"bufio"
19+
"bytes"
1920
"context"
21+
"encoding/base64"
2022
"encoding/json"
21-
"errors"
22-
"fmt"
2323
"io"
2424
"os"
2525
"regexp"
@@ -30,6 +30,7 @@ import (
3030
"github.com/spf13/cobra"
3131

3232
"github.com/slsa-framework/slsa-github-generator/github"
33+
"github.com/slsa-framework/slsa-github-generator/internal/errors"
3334
"github.com/slsa-framework/slsa-github-generator/internal/utils"
3435
"github.com/slsa-framework/slsa-github-generator/signing/sigstore"
3536
"github.com/slsa-framework/slsa-github-generator/slsa"
@@ -47,11 +48,41 @@ var (
4748
provenanceOnlyBuildType = "https://github.com/slsa-framework/slsa-github-generator@v1"
4849
)
4950

51+
// errBase64 indicates a base64 error in the subject.
52+
type errBase64 struct {
53+
errors.WrappableError
54+
}
55+
56+
// errSha indicates a error in the hash format.
57+
type errSha struct {
58+
errors.WrappableError
59+
}
60+
61+
// errNoName indicates a missing subject name.
62+
type errNoName struct {
63+
errors.WrappableError
64+
}
65+
66+
// errDuplicateSubject indicates a duplicate subject name.
67+
type errDuplicateSubject struct {
68+
errors.WrappableError
69+
}
70+
71+
// errScan is an error scanning the SHA digest data.
72+
type errScan struct {
73+
errors.WrappableError
74+
}
75+
5076
// parseSubjects parses the value given to the subjects option.
51-
func parseSubjects(subjectsStr string) ([]intoto.Subject, error) {
77+
func parseSubjects(b64str string) ([]intoto.Subject, error) {
5278
var parsed []intoto.Subject
5379

54-
scanner := bufio.NewScanner(strings.NewReader(subjectsStr))
80+
subjects, err := base64.StdEncoding.DecodeString(b64str)
81+
if err != nil {
82+
return nil, errors.Errorf(&errBase64{}, "error decoding subjects (is it base64 encoded?): %w", err)
83+
}
84+
85+
scanner := bufio.NewScanner(bytes.NewReader(subjects))
5586
for scanner.Scan() {
5687
// Split by whitespace, and get values.
5788
parts := wsSplit.Split(strings.TrimSpace(scanner.Text()), 2)
@@ -64,18 +95,18 @@ func parseSubjects(subjectsStr string) ([]intoto.Subject, error) {
6495
}
6596
// Do a sanity check on the SHA to make sure it's a proper hex digest.
6697
if !shaCheck.MatchString(shaDigest) {
67-
return nil, fmt.Errorf("unexpected sha256 hash %q", shaDigest)
98+
return nil, errors.Errorf(&errSha{}, "unexpected sha256 hash format for %q", shaDigest)
6899
}
69100

70101
// Check for the subject name.
71102
if len(parts) == 1 {
72-
return nil, fmt.Errorf("expected subject name for hash %q", shaDigest)
103+
return nil, errors.Errorf(&errNoName{}, "expected subject name for hash %q", shaDigest)
73104
}
74105
name := strings.TrimSpace(parts[1])
75106

76107
for _, p := range parsed {
77108
if p.Name == name {
78-
return nil, fmt.Errorf("duplicate subject: %q", name)
109+
return nil, errors.Errorf(&errDuplicateSubject{}, "duplicate subject %q", name)
79110
}
80111
}
81112

@@ -87,7 +118,7 @@ func parseSubjects(subjectsStr string) ([]intoto.Subject, error) {
87118
})
88119
}
89120
if err := scanner.Err(); err != nil {
90-
return nil, err
121+
return nil, errors.Errorf(&errScan{}, "reading digest: %w", err)
91122
}
92123

93124
return parsed, nil
@@ -194,7 +225,7 @@ run in the context of a Github Actions workflow.`,
194225

195226
c.Flags().StringVarP(&predicatePath, "predicate", "p", "", "Path to write the unsigned provenance predicate.")
196227
c.Flags().StringVarP(&attPath, "signature", "g", "attestation.intoto.jsonl", "Path to write the signed attestation.")
197-
c.Flags().StringVarP(&subjects, "subjects", "s", "", "Formatted list of subjects in the same format as sha256sum.")
228+
c.Flags().StringVarP(&subjects, "subjects", "s", "", "Formatted list of subjects in the same format as sha256sum (base64 encoded).")
198229

199230
return c
200231
}

internal/builders/generic/attest_test.go

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package main
22

33
import (
4-
"reflect"
54
"testing"
65

6+
"github.com/google/go-cmp/cmp"
7+
"github.com/google/go-cmp/cmp/cmpopts"
78
intoto "github.com/in-toto/in-toto-golang/in_toto"
89
slsav02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
10+
"github.com/slsa-framework/slsa-github-generator/internal/errors"
911
)
1012

1113
// TestParseSubjects tests the parseSubjects function.
@@ -14,11 +16,12 @@ func TestParseSubjects(t *testing.T) {
1416
name string
1517
str string
1618
expected []intoto.Subject
17-
err bool
19+
err error
1820
}{
1921
{
2022
name: "single",
21-
str: "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge",
23+
// echo "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge" | base64 -w0
24+
str: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiBob2dlCg==",
2225
expected: []intoto.Subject{
2326
{
2427
Name: "hoge",
@@ -30,7 +33,8 @@ func TestParseSubjects(t *testing.T) {
3033
},
3134
{
3235
name: "name has spaces",
33-
str: "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge fuga",
36+
// echo "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge fuga" | base64 -w0
37+
str: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiBob2dlIGZ1Z2EK",
3438
expected: []intoto.Subject{
3539
{
3640
Name: "hoge fuga",
@@ -42,7 +46,8 @@ func TestParseSubjects(t *testing.T) {
4246
},
4347
{
4448
name: "extra whitespace",
45-
str: "\t 2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 \t hoge fuga \t ",
49+
// echo -e "\t 2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 \t hoge fuga \t " | base64 -w0
50+
str: "CSAgMmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiAJIGhvZ2UgZnVnYSAgCSAgCg==",
4651
expected: []intoto.Subject{
4752
{
4853
Name: "hoge fuga",
@@ -55,8 +60,8 @@ func TestParseSubjects(t *testing.T) {
5560

5661
{
5762
name: "multiple",
58-
str: `2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge
59-
e712aff3705ac314b9a890e0ec208faa20054eee514d86ab913d768f94e01279 fuga`,
63+
// echo -e "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge\ne712aff3705ac314b9a890e0ec208faa20054eee514d86ab913d768f94e01279 fuga" | base64 -w0
64+
str: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiBob2dlCmU3MTJhZmYzNzA1YWMzMTRiOWE4OTBlMGVjMjA4ZmFhMjAwNTRlZWU1MTRkODZhYjkxM2Q3NjhmOTRlMDEyNzkgZnVnYQo=",
6065
expected: []intoto.Subject{
6166
{
6267
Name: "hoge",
@@ -79,9 +84,8 @@ e712aff3705ac314b9a890e0ec208faa20054eee514d86ab913d768f94e01279 fuga`,
7984
},
8085
{
8186
name: "blank lines",
82-
str: `2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge
83-
84-
e712aff3705ac314b9a890e0ec208faa20054eee514d86ab913d768f94e01279 fuga`,
87+
// echo -e "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge\n\ne712aff3705ac314b9a890e0ec208faa20054eee514d86ab913d768f94e01279 fuga" | base64 -w0
88+
str: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiBob2dlCgplNzEyYWZmMzcwNWFjMzE0YjlhODkwZTBlYzIwOGZhYTIwMDU0ZWVlNTE0ZDg2YWI5MTNkNzY4Zjk0ZTAxMjc5IGZ1Z2EK",
8589
expected: []intoto.Subject{
8690
{
8791
Name: "hoge",
@@ -99,36 +103,41 @@ e712aff3705ac314b9a890e0ec208faa20054eee514d86ab913d768f94e01279 fuga`,
99103
},
100104
{
101105
name: "sha only",
102-
str: "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2",
103-
err: true,
106+
// echo "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2" | base64 -w0
107+
str: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMgo=",
108+
err: &errNoName{},
104109
},
105110
{
106111
name: "invalid hash",
107-
str: "abcdef hoge",
108-
err: true,
112+
// echo "abcdef hoge" | base64 -w0
113+
str: "YWJjZGVmIGhvZ2UK",
114+
err: &errSha{},
109115
},
110116
{
111117
name: "duplicate name",
112-
str: `2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge
113-
2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge`,
114-
err: true,
118+
// echo -e "2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge\n2e0390eb024a52963db7b95e84a9c2b12c004054a7bad9a97ec0c7c89d4681d2 hoge" | base64 -w0
119+
str: "MmUwMzkwZWIwMjRhNTI5NjNkYjdiOTVlODRhOWMyYjEyYzAwNDA1NGE3YmFkOWE5N2VjMGM3Yzg5ZDQ2ODFkMiBob2dlCjJlMDM5MGViMDI0YTUyOTYzZGI3Yjk1ZTg0YTljMmIxMmMwMDQwNTRhN2JhZDlhOTdlYzBjN2M4OWQ0NjgxZDIgaG9nZQo=",
120+
err: &errDuplicateSubject{},
121+
},
122+
{
123+
name: "not base64",
124+
str: "this is not base64",
125+
err: &errBase64{},
115126
},
116127
}
117128

118129
for _, tc := range testCases {
119130
t.Run(tc.name, func(t *testing.T) {
120131
if s, err := parseSubjects(tc.str); err != nil {
121-
if tc.err {
122-
// Error was expected.
123-
return
132+
if tc.err != nil && !errors.As(err, &tc.err) {
133+
t.Fatalf("unexpected error: %v", cmp.Diff(err, tc.err, cmpopts.EquateErrors()))
124134
}
125-
t.Fatalf("unexpected error: %v", err)
126135
} else {
127-
if tc.err {
128-
t.Fatalf("expected error but received %#v", s)
136+
if tc.err != nil {
137+
t.Fatalf("expected %#v but received %#v", tc.err, s)
129138
}
130139

131-
if want, got := tc.expected, s; !reflect.DeepEqual(want, got) {
140+
if want, got := tc.expected, s; !cmp.Equal(want, got) {
132141
t.Errorf("unexpected subjects, want: %#v, got: %#v", want, got)
133142
}
134143
}

0 commit comments

Comments
 (0)