Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for BYOB verification #562

Merged
merged 5 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions cli/slsa-verifier/main_regression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ func Test_runVerifyGHAArtifactPath(t *testing.T) {

// Validate against test's expected builderID, if provided.
if tt.outBuilderID != "" {
if err := outBuilderID.Matches(tt.outBuilderID, false); err != nil {
if err := outBuilderID.MatchesLoose(tt.outBuilderID, false); err != nil {
t.Errorf(fmt.Sprintf("matches failed (1): %v", err))
}
}
Expand Down Expand Up @@ -655,7 +655,7 @@ func Test_runVerifyGHAArtifactPath(t *testing.T) {
// resulting builderID returned by the provenance check.
// Since this a GHA and the certificate ID is in long form,
// we pass `allowRef = true`.
if err := outBuilderID.Matches(*bid, true); err != nil {
if err := outBuilderID.MatchesLoose(*bid, true); err != nil {
t.Errorf(fmt.Sprintf("matches failed (2): %v", err))
}
}
Expand Down Expand Up @@ -814,7 +814,7 @@ func Test_runVerifyGHAArtifactImage(t *testing.T) {

// Validate against test's expected builderID, if provided.
if tt.outBuilderID != "" {
if err := outBuilderID.Matches(tt.outBuilderID, false); err != nil {
if err := outBuilderID.MatchesLoose(tt.outBuilderID, false); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
}
Expand All @@ -827,7 +827,7 @@ func Test_runVerifyGHAArtifactImage(t *testing.T) {
// resulting builderID returned by the provenance check.
// Since this a GHA and the certificate ID is in long form,
// we pass `allowRef = true`.
if err := outBuilderID.Matches(*bid, true); err != nil {
if err := outBuilderID.MatchesLoose(*bid, true); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
}
Expand Down Expand Up @@ -1283,13 +1283,13 @@ func Test_runVerifyGCBArtifactImage(t *testing.T) {

// Validate against test's expected builderID, if provided.
if tt.outBuilderID != "" {
if err := outBuilderID.Matches(tt.outBuilderID, false); err != nil {
if err := outBuilderID.MatchesLoose(tt.outBuilderID, false); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
}

// Validate against builderID we generated automatically.
if err := outBuilderID.Matches(bid, false); err != nil {
if err := outBuilderID.MatchesLoose(bid, false); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
}
Expand Down
1 change: 1 addition & 0 deletions cli/slsa-verifier/verify/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func (o *VerifyNpmOptions) AddFlags(cmd *cobra.Command) {
"[optional] print the verified provenance to stdout")

cmd.MarkFlagRequired("source-uri")
cmd.MarkFlagRequired("builder-id")
cmd.MarkFlagsMutuallyExclusive("source-versioned-tag", "source-tag")
}

Expand Down
4 changes: 2 additions & 2 deletions verifiers/internal/gcb/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,14 @@ func (p *Provenance) VerifyBuilder(builderOpts *options.BuilderOpts) (*utils.Tru
return nil, err
}

provBuilderID, err := utils.TrustedBuilderIDNew(predicateBuilderID)
provBuilderID, err := utils.TrustedBuilderIDNew(predicateBuilderID, true)
if err != nil {
return nil, err
}

// Validate with user-provided value.
if builderOpts != nil && builderOpts.ExpectedID != nil {
if err := provBuilderID.Matches(*builderOpts.ExpectedID, false); err != nil {
if err := provBuilderID.MatchesLoose(*builderOpts.ExpectedID, false); err != nil {
return nil, err
}
}
Expand Down
6 changes: 3 additions & 3 deletions verifiers/internal/gcb/provenance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ func Test_VerifyBuilder(t *testing.T) {
panic("outBuilderID is nil")
}

if err := outBuilderID.Matches(tt.builderID, false); err != nil {
if err := outBuilderID.MatchesLoose(tt.builderID, false); err != nil {
t.Errorf(fmt.Sprintf("matches failed: %v", err))
}
})
Expand Down Expand Up @@ -299,7 +299,7 @@ func Test_validateRecipeType(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

builderID, err := utils.TrustedBuilderIDNew(tt.builderID)
builderID, err := utils.TrustedBuilderIDNew(tt.builderID, true)
if err != nil {
panic(fmt.Errorf("BuilderIDNew: %w", err))
}
Expand Down Expand Up @@ -489,7 +489,7 @@ func Test_VerifySourceURI(t *testing.T) {
panic(fmt.Errorf("setStatement: %w", err))
}

builderID, err := utils.TrustedBuilderIDNew(tt.builderID)
builderID, err := utils.TrustedBuilderIDNew(tt.builderID, true)
if err != nil {
panic(fmt.Errorf("BuilderIDNew: %w", err))
}
Expand Down
18 changes: 11 additions & 7 deletions verifiers/internal/gha/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ var (
trustedBuilderRepository = "slsa-framework/slsa-github-generator"
e2eTestRepository = "slsa-framework/example-package"
certOidcIssuer = "https://token.actions.githubusercontent.com"
githubCom = "github.com/"
httpsGithubCom = "https://" + githubCom
// This is used in cosign's CheckOpts for validating the certificate. We
// do specific builder verification after this.
certSubjectRegexp = "https://github.com/*"
certSubjectRegexp = httpsGithubCom + "*"
)

var defaultArtifactTrustedReusableWorkflows = map[string]bool{
Expand All @@ -32,8 +34,10 @@ var defaultContainerTrustedReusableWorkflows = map[string]bool{
trustedBuilderRepository + "/.github/workflows/generator_container_slsa3.yml": true,
}

var delegatorGenericReusableWorkflow = trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml"

var defaultBYOBReusableWorkflows = map[string]bool{
trustedBuilderRepository + "/.github/workflows/delegator_generic_slsa3.yml": true,
delegatorGenericReusableWorkflow: true,
}

// VerifyCertficateSourceRepository verifies the source repository.
Expand All @@ -43,7 +47,7 @@ func VerifyCertficateSourceRepository(id *WorkflowIdentity,
// The caller repository in the x509 extension is not fully qualified. It only contains
// {org}/{repository}.
expectedSource := strings.TrimPrefix(sourceRepo, "git+https://")
expectedSource = strings.TrimPrefix(expectedSource, "github.com/")
expectedSource = strings.TrimPrefix(expectedSource, githubCom)
if id.CallerRepository != expectedSource {
return fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource,
expectedSource, id.CallerRepository)
Expand Down Expand Up @@ -93,7 +97,7 @@ func VerifyBuilderIdentity(id *WorkflowIdentity,
func verifyTrustedBuilderID(certPath, certTag string, expectedBuilderID *string, defaultBuilders map[string]bool) (*utils.TrustedBuilderID, error) {
var trustedBuilderID *utils.TrustedBuilderID
var err error
certBuilderName := "https://github.com/" + certPath
certBuilderName := httpsGithubCom + certPath
// WARNING: we don't validate the tag here, because we need to allow
// refs/heads/main for e2e tests. See verifyTrustedBuilderRef().
// No builder ID provided by user: use the default trusted workflows.
Expand All @@ -102,22 +106,22 @@ func verifyTrustedBuilderID(certPath, certTag string, expectedBuilderID *string,
return nil, fmt.Errorf("%w: %s got %t", serrors.ErrorUntrustedReusableWorkflow, certPath, expectedBuilderID == nil)
}
// Construct the builderID using the certificate's builder's name and tag.
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName + "@" + certTag)
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName+"@"+certTag, true)
if err != nil {
return nil, err
}
} else {
// Verify the builderID.
// We only accept IDs on github.com.
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName + "@" + certTag)
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderName+"@"+certTag, true)
if err != nil {
return nil, err
}

// BuilderID provided by user should match the certificate.
// Note: the certificate builderID has the form `name@refs/tags/v1.2.3`,
// so we pass `allowRef = true`.
if err := trustedBuilderID.Matches(*expectedBuilderID, true); err != nil {
if err := trustedBuilderID.MatchesLoose(*expectedBuilderID, true); err != nil {
return nil, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
}
}
Expand Down
5 changes: 2 additions & 3 deletions verifiers/internal/gha/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func Test_VerifyBuilderIdentity(t *testing.T) {
return
}

if err := id.Matches(tt.builderID, true); err != nil {
if err := id.MatchesLoose(tt.builderID, true); err != nil {
t.Errorf("matches failed:%v", err)
}
})
Expand Down Expand Up @@ -480,7 +480,7 @@ func Test_verifyTrustedBuilderID(t *testing.T) {
return
}
expectedID := "https://github.com/" + tt.path + "@" + tt.tag
if err := id.Matches(expectedID, true); err != nil {
if err := id.MatchesLoose(expectedID, true); err != nil {
t.Errorf("matches failed:%v", err)
}
})
Expand Down Expand Up @@ -607,7 +607,6 @@ func Test_verifyTrustedBuilderRef(t *testing.T) {
expected: serrors.ErrorInvalidRef,
},
{

name: "full semver for other repos",
callerRepo: "some/repo",
builderRef: "refs/tags/v1.2.3",
Expand Down
4 changes: 1 addition & 3 deletions verifiers/internal/gha/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

const (
publishAttestationV01 = "https://github.com/npm/attestation/tree/main/specs/publish/"
builderGitHubRunnerID = "https://github.com/actions/runner"
)

var errrorInvalidAttestations = errors.New("invalid npm attestations")
Expand Down Expand Up @@ -64,9 +65,6 @@ type Npm struct {
publishAttestation *attestation
}

// TODO(#494): update the builder name.
var builderGitHubRunnerID = "https://github.com/actions/runner@v0.1"

func (n *Npm) ProvenanceEnvelope() *dsse.Envelope {
return n.verifiedProvenanceAtt.Envelope
}
Expand Down
51 changes: 32 additions & 19 deletions verifiers/internal/gha/provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,38 @@ func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) {
}

// Verify Builder ID in provenance statement.
// This function does an exact comparison, and expects certBuilderID to be the full
// This function does an exact comparison, and expects expectedBuilderID to be the full
// `name@refs/tags/<name>`.
func verifyBuilderIDExactMatch(prov slsaprovenance.Provenance, certBuilderID string) error {
builderID, err := prov.BuilderID()
func verifyBuilderIDExactMatch(prov slsaprovenance.Provenance, expectedBuilderID string) error {
id, err := prov.BuilderID()
if err != nil {
return err
}
if certBuilderID != builderID {
return fmt.Errorf("%w: expected '%s' in builder.id, got '%s'", serrors.ErrorMismatchBuilderID,
certBuilderID, builderID)
provBuilderID, err := utils.TrustedBuilderIDNew(id, false)
if err != nil {
return err
}
if err := provBuilderID.MatchesFull(expectedBuilderID, true); err != nil {
return fmt.Errorf("%w", err)
}
return nil
}

// Verify Builder ID in provenance statement.
// This function verifies the names match. If the expected builder ID contains a version,
// it also verifies the versions match.
func verifyBuilderIDLooseMatch(prov slsaprovenance.Provenance, expectedBuilderID string) error {
id, err := prov.BuilderID()
if err != nil {
return err
}
provBuilderID, err := utils.TrustedBuilderIDNew(id, false)
if err != nil {
return err
}
if err := provBuilderID.MatchesLoose(expectedBuilderID, true); err != nil {
return fmt.Errorf("%w", err)
}
return nil
}

Expand Down Expand Up @@ -207,21 +227,14 @@ func VerifyNpmPackageProvenance(env *dsselib.Envelope, provenanceOpts *options.P
return err
}

// Untrusted builder.
if provenanceOpts.ExpectedBuilderID == "" {
// Verify it's the npm CLI.
builderID, err := prov.BuilderID()
if err != nil {
return err
}
// TODO(#494): update the builder ID string.
if !strings.HasPrefix(builderID, "https://github.com/npm/cli@") {
return fmt.Errorf("%w: expected 'https://github.com/npm/cli' in builder.id, got '%s'",
serrors.ErrorMismatchBuilderID, builderID)
}
} else if err := verifyBuilderIDExactMatch(prov, provenanceOpts.ExpectedBuilderID); err != nil {
// TODO: Verify the buildType.
// This depends on the builder (delegator or CLI).

// Verify the builder ID.
if err := verifyBuilderIDLooseMatch(prov, provenanceOpts.ExpectedBuilderID); err != nil {
return err
}

// NOTE: for the non trusted builders, the information may be forgeable.
// Also, the GitHub context is not recorded for the default builder.
return VerifyProvenanceCommonOptions(prov, provenanceOpts, true)
Expand Down
52 changes: 39 additions & 13 deletions verifiers/internal/gha/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func GHAVerifierNew() *GHAVerifier {
// generated by the builderID.
func (v *GHAVerifier) IsAuthoritativeFor(builderID string) bool {
// This verifier only supports builders defined on GitHub.
return strings.HasPrefix(builderID, "https://github.com/")
return strings.HasPrefix(builderID, httpsGithubCom)
}

func verifyEnvAndCert(env *dsse.Envelope,
Expand Down Expand Up @@ -98,7 +98,14 @@ func verifyNpmEnvAndCert(env *dsse.Envelope,
}

// Verify the workflow identity.
trustedBuilderID, err := VerifyBuilderIdentity(workflowInfo, builderOpts, defaultBuilders)
// We verify against the delegator re-usable workflow, not the user-provided
// builder. This is because the signing identity for delegator-based builders
// is *always* the delegator workflow.
expectedDelegatorWorkflow := httpsGithubCom + delegatorGenericReusableWorkflow
delegatorBuilderOpts := options.BuilderOpts{
ExpectedID: &expectedDelegatorWorkflow,
}
trustedBuilderID, err := VerifyBuilderIdentity(workflowInfo, &delegatorBuilderOpts, defaultBuilders)
// We accept a non-trusted builder for the default npm builder
// that uses npm CLI.
if err != nil && !errors.Is(err, serrors.ErrorUntrustedReusableWorkflow) {
Expand All @@ -113,25 +120,44 @@ func verifyNpmEnvAndCert(env *dsse.Envelope,
return nil, err
}

// Verify properties of the SLSA provenance.
// Unpack and verify info in the provenance, including the Subject Digest.
// WARNING: builderID may be empty if it's not a reusable builder workflow.
// Users must always provide the builder ID.
if builderOpts == nil || builderOpts.ExpectedID == nil {
return nil, fmt.Errorf("builder ID is empty")
}

// WARNING: builderID may be empty if it's not a trusted reusable builder workflow.
if trustedBuilderID != nil {
provenanceOpts.ExpectedBuilderID = trustedBuilderID.String()
// We only support builders built using the BYOB framework.
// The builder is guaranteed to be delegatorGenericReusableWorkflow, since this is the builder
// we compare against in the call to VerifyBuilderIdentity() above.
// The delegator workflow will set the builder ID to the caller's path,
// which is what users match against.
provenanceOpts.ExpectedBuilderID = *builderOpts.ExpectedID
} else {
if builderOpts == nil || builderOpts.ExpectedID == nil {
return nil, fmt.Errorf("builder mistmatch. No builder ID provided by user , got '%v'", builderGitHubRunnerID)
}
// TODO(#494): update the builder ID name.
trustedBuilderID, err = utils.TrustedBuilderIDNew(builderGitHubRunnerID)
// NOTE: if the user created provenance using a re-usable workflow
// that does not integrate with the BYOB framework, this code will be run.
// In this case, the re-usable workflow must set the builder ID to
// builderGitHubRunnerID. This means we treat arbitrary re-usable
// workflows like the default GitHub Action runner. Note that
// the SAN in the certificate is *different* from the builder ID
// provided by users during verification.
// We may add support for verifying provenance from arbitrary re-usable workflows
// later; which may be useful for org-level builders.

// The builder.id is set to builderGitHubRunnerID by the npm CLI.
trustedBuilderID, err = utils.TrustedBuilderIDNew(builderGitHubRunnerID, false)
if err != nil {
return nil, err
}
if err := trustedBuilderID.Matches(*builderOpts.ExpectedID, false); err != nil {
return nil, fmt.Errorf("builder mistmatch. %w", err)
if err := trustedBuilderID.MatchesLoose(*builderOpts.ExpectedID, false); err != nil {
return nil, fmt.Errorf("%w", err)
}
// On GitHub we only support the default GitHub runner builder.
provenanceOpts.ExpectedBuilderID = builderGitHubRunnerID
}

// Verify properties of the SLSA provenance.
// Unpack and verify info in the provenance, including the Subject Digest.
if err := VerifyNpmPackageProvenance(env, provenanceOpts); err != nil {
return nil, err
}
Expand Down
Loading