Skip to content

Commit 8bcf1f0

Browse files
enteraga6Ian Lewislaurentsimon
authored
feat: Non-compulsory BuilderID for BYOB Builders (#674)
/cc @mihaimaruseac /cc @laurentsimon Based off the prefix of the BuilderID within the provenance, if the builder use to build the artifact is one of the BYOB builders of slsa-framework/slsa-github-generator repo, the --builderid flag is not need and is handled automatically. This was done to increase access to users since before the automatic pickup of the builder-id would get the delegator. Test cases that cover verifyProvenance will need to be complete after the v1.8.0 release of slsa-framework/slsa-github-generator. The main structure that is changed is the ExpectedBuilderPath is hardcoded now to slsa-framework builders within `/cli/slsa-verifier/verify/verify_artifact.go `. This can later be changed now if needed to be an input like the other fields of `provenanceOpts` populated during `verify_artifact.go`. The added function within `provenance.go`, `verifyBuilderIDPath` is called during `verifyProvenance` to check this path within `provenanceOpts`. Upon failure of this function, expected and received BuilderID's are also outputted. closes #659 makes use of discussion on closed pr #673 --------- Signed-off-by: Noah Elzner <[email protected]> Signed-off-by: Noah Elzner <[email protected]> Co-authored-by: Ian Lewis <[email protected]> Co-authored-by: laurentsimon <[email protected]>
1 parent 57e3f65 commit 8bcf1f0

8 files changed

+242
-31
lines changed

options/options.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type ProvenanceOpts struct {
1818
// ExpectedSourceURI is the expected source URI in the provenance.
1919
ExpectedSourceURI string
2020

21-
// ExpectedBuilderID is the expected builder ID.
21+
// ExpectedBuilderID is the expected builder ID that is passed from user and verified
2222
ExpectedBuilderID string
2323

2424
// ExpectedWorkflowInputs is a map of key=value inputs.
@@ -31,6 +31,6 @@ type ProvenanceOpts struct {
3131

3232
// BuildOpts are the options for checking the builder.
3333
type BuilderOpts struct {
34-
// ExpectedID is the expected builder ID.
34+
// ExpectedBuilderID is the builderID passed in from the user to be verified
3535
ExpectedID *string
3636
}

verifiers/internal/gha/builder.go

+27-16
Original file line numberDiff line numberDiff line change
@@ -109,34 +109,45 @@ func verifyTrustedBuilderID(certBuilderID, certTag string, expectedBuilderID *st
109109
if _, ok := defaultTrustedBuilders[certBuilderID]; !ok {
110110
return nil, false, fmt.Errorf("%w: %s with builderID provided: %t", serrors.ErrorUntrustedReusableWorkflow, certBuilderID, expectedBuilderID != nil)
111111
}
112+
112113
// Construct the builderID using the certificate's builder's name and tag.
113114
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
114115
if err != nil {
115116
return nil, false, err
116117
}
117-
} else {
118-
// Verify the builderID.
119-
// We only accept IDs on github.com.
120-
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
121-
if err != nil {
122-
return nil, false, err
123-
}
124118

125119
// Check if:
126120
// - the builder in the cert is a BYOB builder
127121
// - the caller trusts the BYOB builder
128122
// If both are true, we don't match the user-provided builder ID
129123
// against the certificate. Instead that will be done by the caller.
130-
if isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders) {
131-
return trustedBuilderID, true, nil
132-
}
124+
//
125+
// This return of the delegator builderID enables non-compulsory
126+
// builderID feature for BYOB builders by setting byob flag to true.
127+
return trustedBuilderID, isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders), nil
128+
}
133129

134-
// Not a BYOB builder. BuilderID provided by user should match the certificate.
135-
// Note: the certificate builderID has the form `name@refs/tags/v1.2.3`,
136-
// so we pass `allowRef = true`.
137-
if err := trustedBuilderID.MatchesLoose(*expectedBuilderID, true); err != nil {
138-
return nil, false, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
139-
}
130+
// Verify the builderID.
131+
// We only accept IDs on github.com.
132+
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
133+
if err != nil {
134+
return nil, false, err
135+
}
136+
137+
// Check if:
138+
// - the builder in the cert is a BYOB builder
139+
// - the caller trusts the BYOB builder
140+
// If both are true, we don't match the user-provided builder ID
141+
// against the certificate. Instead that will be done by the caller.
142+
if isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders) {
143+
return trustedBuilderID, true, nil
144+
}
145+
146+
// Not a BYOB builder. BuilderID provided by user should match the certificate.
147+
// Note: the certificate builderID has the form `name@refs/tags/v1.2.3`,
148+
// so we pass `allowRef = true`.
149+
if err := trustedBuilderID.MatchesLoose(*expectedBuilderID, true); err != nil {
150+
return nil, false, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
140151
}
141152

142153
return trustedBuilderID, false, nil

verifiers/internal/gha/builder_test.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,17 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
477477
defaults: defaultBYOBReusableWorkflows,
478478
byob: true,
479479
},
480+
{
481+
// This is a BYOB workflow without an id that tests non-compulsory builder-id
482+
// feature of slsa-verifier and expects byob to be true
483+
name: "generic delegator workflow no id",
484+
path: trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml",
485+
// NOTE: id is nil.
486+
id: nil,
487+
tag: "refs/tags/v1.2.3",
488+
defaults: defaultBYOBReusableWorkflows,
489+
byob: true,
490+
},
480491
{
481492
name: "low perms delegator workflow short tag",
482493
path: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml",
@@ -486,10 +497,15 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
486497
byob: true,
487498
},
488499
{
489-
name: "low perms delegator workflow no ID provided",
490-
path: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml",
500+
// This is a BYOB workflow without an id that tests non-compulsory builder-id
501+
// feature of slsa-verifier and expects byob to be true
502+
name: "low perms delegator workflow no ID provided",
503+
path: trustedBuilderRepository + "/.github/workflows/delegator_lowperms-generic_slsa3.yml",
504+
// NOTE: id is nil.
505+
id: nil,
491506
tag: "v1.2.3",
492507
defaults: defaultBYOBReusableWorkflows,
508+
byob: true,
493509
},
494510
{
495511
name: "default mismatch against container defaults long tag",

verifiers/internal/gha/provenance.go

+42-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ func verifyBuilderIDExactMatch(prov iface.Provenance, expectedBuilderID string)
5858
return nil
5959
}
6060

61+
// verifyBuilderIDPathPrefix verifies that the builder ID in provenance matches the provided expectedBuilderIDPathPrefix.
62+
// Returns provenance builderID if verified against provided expected Builder ID path prefix.
63+
func verifyBuilderIDPathPrefix(prov iface.Provenance, expectedBuilderIDPathPrefix string) (string, error) {
64+
id, err := prov.BuilderID()
65+
if err != nil {
66+
return "", err
67+
}
68+
69+
provBuilderID, err := utils.TrustedBuilderIDNew(id, false)
70+
if err != nil {
71+
return "", err
72+
}
73+
74+
// Compare actual BuilderID with the expected BuilderID Path Prefix.
75+
if !strings.HasPrefix(provBuilderID.Name(), expectedBuilderIDPathPrefix) {
76+
return "", fmt.Errorf("%w: BuilderID Path Mismatch. Got: %q. Expected BuilderID Path Prefix: %q", serrors.ErrorInvalidBuilderID, provBuilderID.Name(), expectedBuilderIDPathPrefix)
77+
}
78+
79+
return provBuilderID.Name(), nil
80+
}
81+
6182
// Verify Builder ID in provenance statement.
6283
// This function verifies the names match. If the expected builder ID contains a version,
6384
// it also verifies the versions match.
@@ -70,6 +91,7 @@ func verifyBuilderIDLooseMatch(prov iface.Provenance, expectedBuilderID string)
7091
if err != nil {
7192
return err
7293
}
94+
7395
if err := provBuilderID.MatchesLoose(expectedBuilderID, true); err != nil {
7496
return err
7597
}
@@ -304,7 +326,8 @@ func builderID(env *dsselib.Envelope, certTrustedBuilderID *utils.TrustedBuilder
304326
}
305327

306328
// VerifyProvenance verifies the provenance for the given DSSE envelope.
307-
func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts, trustedBuilderID *utils.TrustedBuilderID, byob bool) error {
329+
func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts, trustedBuilderID *utils.TrustedBuilderID, byob bool,
330+
expectedID *string) error {
308331
prov, err := slsaprovenance.ProvenanceFromEnvelope(trustedBuilderID.Name(), env)
309332
if err != nil {
310333
return err
@@ -315,7 +338,24 @@ func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceO
315338
if err := isValidDelegatorBuilderID(prov); err != nil {
316339
return err
317340
}
318-
// Note: `provenanceOpts.ExpectedBuilderID` is provided by the user.
341+
342+
// If expectedID is not provided, check to see if it is a trusted builder.
343+
// If not provided, then a trusted builder is expected, to populate provenanceOpts.ExpectedBuilderID
344+
// with that builder, otherwise, populate from user input.
345+
//
346+
// This can verify the actual BYOB builderIDPath against the trusted builderIDPath provided.
347+
// Currently slsa-framework path is the only one supported for ExpectedBuilderPath.
348+
if expectedID == nil {
349+
var trustedBuilderRepositoryPath = httpsGithubCom + trustedBuilderRepository + "/.github/workflows/"
350+
if provenanceOpts.ExpectedBuilderID, err = verifyBuilderIDPathPrefix(prov, trustedBuilderRepositoryPath); err != nil {
351+
return err
352+
}
353+
} else {
354+
provenanceOpts.ExpectedBuilderID = *expectedID
355+
}
356+
357+
// NOTE: `provenanceOpts.ExpectedBuilderID` is provided by the user
358+
// or from return of verifyBuilderIDPath.
319359
if err := verifyBuilderIDLooseMatch(prov, provenanceOpts.ExpectedBuilderID); err != nil {
320360
return err
321361
}

verifiers/internal/gha/provenance_test.go

+145
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gha
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57
"time"
68

@@ -9,6 +11,8 @@ import (
911
slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
1012
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
1113
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
14+
"github.com/slsa-framework/slsa-verifier/v2/options"
15+
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
1216

1317
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
1418
"github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common"
@@ -1182,3 +1186,144 @@ func Test_VerifyVersionedTag(t *testing.T) {
11821186
})
11831187
}
11841188
}
1189+
1190+
func Test_VerifyProvenance(t *testing.T) {
1191+
t.Parallel()
1192+
tests := []struct {
1193+
name string
1194+
envelopePath string
1195+
provenanceOpts *options.ProvenanceOpts
1196+
trustedBuilderIDName string
1197+
byob bool
1198+
expectedID *string
1199+
expected error
1200+
}{
1201+
{
1202+
name: "Verify Trusted (slsa-github-generator) Bazel Builder (v1.8.0)",
1203+
envelopePath: "bazel-trusted-dsseEnvelope.build.slsa",
1204+
provenanceOpts: &options.ProvenanceOpts{
1205+
ExpectedBranch: nil,
1206+
ExpectedTag: nil,
1207+
ExpectedVersionedTag: nil,
1208+
ExpectedDigest: "caaadba2846905ac477c777e96a636e1c2e067fdf6fed90ec9eeca4df18d6ed9",
1209+
ExpectedSourceURI: "github.com/enteraga6/slsa-lvl3-generic-provenance-with-bazel-example",
1210+
ExpectedBuilderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.8.0",
1211+
ExpectedWorkflowInputs: map[string]string{},
1212+
},
1213+
byob: true,
1214+
trustedBuilderIDName: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.8.0",
1215+
expectedID: nil,
1216+
},
1217+
{
1218+
name: "Verify Un-Trusted (slsa-github-generator) Bazel Builder (from enteraga6/slsa-github-generator)",
1219+
envelopePath: "bazel-untrusted-dsseEnvelope.sigstore",
1220+
provenanceOpts: &options.ProvenanceOpts{
1221+
ExpectedBranch: nil,
1222+
ExpectedTag: nil,
1223+
ExpectedVersionedTag: nil,
1224+
ExpectedDigest: "caaadba2846905ac477c777e96a636e1c2e067fdf6fed90ec9eeca4df18d6ed9",
1225+
ExpectedSourceURI: "github.com/enteraga6/slsa-lvl3-generic-provenance-with-bazel-example",
1226+
ExpectedBuilderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.7.0",
1227+
ExpectedWorkflowInputs: map[string]string{},
1228+
},
1229+
byob: true,
1230+
trustedBuilderIDName: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.7.0",
1231+
expectedID: nil,
1232+
expected: serrors.ErrorInvalidBuilderID,
1233+
},
1234+
{
1235+
name: "Verify Trusted - Empty ExpectedBuilderID",
1236+
envelopePath: "bazel-trusted-dsseEnvelope.build.slsa",
1237+
provenanceOpts: &options.ProvenanceOpts{
1238+
ExpectedBranch: nil,
1239+
ExpectedTag: nil,
1240+
ExpectedVersionedTag: nil,
1241+
ExpectedDigest: "caaadba2846905ac477c777e96a636e1c2e067fdf6fed90ec9eeca4df18d6ed9",
1242+
ExpectedSourceURI: "github.com/enteraga6/slsa-lvl3-generic-provenance-with-bazel-example",
1243+
ExpectedBuilderID: "",
1244+
ExpectedWorkflowInputs: map[string]string{},
1245+
},
1246+
byob: true,
1247+
trustedBuilderIDName: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.8.0",
1248+
expectedID: nil,
1249+
},
1250+
}
1251+
for _, tt := range tests {
1252+
tt := tt // Re-initializing variable so it is not changed while executing the closure below
1253+
t.Run(tt.name, func(t *testing.T) {
1254+
t.Parallel()
1255+
trustedBuilderID, tErr := utils.TrustedBuilderIDNew(tt.trustedBuilderIDName, true)
1256+
if tErr != nil {
1257+
t.Errorf("Provenance Verification FAILED. Error: %v", tErr)
1258+
}
1259+
1260+
envelopeBytes, err := os.ReadFile(filepath.Join("testdata", tt.envelopePath))
1261+
if err != nil {
1262+
t.Errorf("os.ReadFile: %v", err)
1263+
}
1264+
1265+
env, err := EnvelopeFromBytes(envelopeBytes)
1266+
if err != nil {
1267+
t.Errorf("unexpected error parsing envelope %v", err)
1268+
}
1269+
1270+
if err := VerifyProvenance(env, tt.provenanceOpts, trustedBuilderID, tt.byob, tt.expectedID); !errCmp(err, tt.expected) {
1271+
t.Errorf(cmp.Diff(err, tt.expected))
1272+
}
1273+
})
1274+
}
1275+
}
1276+
1277+
func Test_VerifyUntrustedProvenance(t *testing.T) {
1278+
t.Parallel()
1279+
tests := []struct {
1280+
name string
1281+
envelopePath string
1282+
provenanceOpts *options.ProvenanceOpts
1283+
trustedBuilderIDName string
1284+
byob bool
1285+
expectedID *string
1286+
expected error
1287+
}{
1288+
{
1289+
name: "Verify Un-Trusted (slsa-github-generator) Bazel Builder (from enteraga6/slsa-github-generator)",
1290+
envelopePath: "bazel-untrusted-dsseEnvelope.sigstore",
1291+
provenanceOpts: &options.ProvenanceOpts{
1292+
ExpectedBranch: nil,
1293+
ExpectedTag: nil,
1294+
ExpectedVersionedTag: nil,
1295+
ExpectedDigest: "caaadba2846905ac477c777e96a636e1c2e067fdf6fed90ec9eeca4df18d6ed9",
1296+
ExpectedSourceURI: "github.com/enteraga6/slsa-lvl3-generic-provenance-with-bazel-example",
1297+
ExpectedBuilderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.7.0",
1298+
ExpectedWorkflowInputs: map[string]string{},
1299+
},
1300+
byob: true,
1301+
trustedBuilderIDName: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/delegator_lowperms-generic_slsa3.yml@refs/tags/v1.7.0",
1302+
expectedID: nil,
1303+
},
1304+
}
1305+
for _, tt := range tests {
1306+
tt := tt // Re-initializing variable so it is not changed while executing the closure below
1307+
t.Run(tt.name, func(t *testing.T) {
1308+
t.Parallel()
1309+
trustedBuilderID, tErr := utils.TrustedBuilderIDNew(tt.trustedBuilderIDName, true)
1310+
if tErr != nil {
1311+
t.Errorf("Provenance Verification FAILED. Error: %v", tErr)
1312+
}
1313+
1314+
envelopeBytes, err := os.ReadFile(filepath.Join("testdata", tt.envelopePath))
1315+
if err != nil {
1316+
t.Errorf("os.ReadFile: %v", err)
1317+
}
1318+
1319+
env, err := EnvelopeFromBytes(envelopeBytes)
1320+
if err != nil {
1321+
t.Errorf("unexpected error parsing envelope %v", err)
1322+
}
1323+
1324+
if err := VerifyProvenance(env, tt.provenanceOpts, trustedBuilderID, tt.byob, tt.expectedID); errCmp(err, tt.expected) {
1325+
t.Errorf(cmp.Diff(err, tt.expected))
1326+
}
1327+
})
1328+
}
1329+
}

0 commit comments

Comments
 (0)