Files
slsa-verifier/verifiers/internal/gha/verifier.go
Appu a481a1974e feat: verify provenance for bcr modules produced by trusted reusable workflows (#840)
@fweikert these are the changes I think might be needed to get this to
work (it's somewhat hacky, I'm not sure I've fully covered what's
needed).

@ramonpetgrave64 is this kinda what's needed?

This now adds the `verify-github-attestation` sub command. Use this
instead of `verify-artifact`.

---------

Signed-off-by: Appu Goundan <appu@google.com>
Signed-off-by: Appu <appu@google.com>
Co-authored-by: Ramon Petgrave <32398091+ramonpetgrave64@users.noreply.github.com>
2025-04-10 14:09:09 -04:00

408 lines
13 KiB
Go

package gha
import (
"context"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"github.com/google/go-containerregistry/pkg/name"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
"github.com/slsa-framework/slsa-verifier/v2/options"
"github.com/slsa-framework/slsa-verifier/v2/register"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils/container"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
)
const VerifierName = "GHA"
//nolint:gochecknoinits
func init() {
register.RegisterVerifier(VerifierName, GHAVerifierNew())
}
type GHAVerifier struct{}
func GHAVerifierNew() *GHAVerifier {
return &GHAVerifier{}
}
// IsAuthoritativeFor returns true of the verifier can verify provenance
// generated by the builderID.
func (v *GHAVerifier) IsAuthoritativeFor(builderID string) bool {
// This verifier only supports builders defined on GitHub.
return strings.HasPrefix(builderID, httpsGithubCom)
}
func verifyEnvAndCert(env *dsse.Envelope,
cert *x509.Certificate,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
defaultBuilders map[string]bool,
) ([]byte, *utils.TrustedBuilderID, error) {
/* Verify properties of the signing identity. */
// Get the workflow info given the certificate information.
workflowInfo, err := GetWorkflowInfoFromCertificate(cert)
if err != nil {
return nil, nil, err
}
// Verify the builder identity.
verifiedBuilderID, byob, err := VerifyBuilderIdentity(workflowInfo, builderOpts, defaultBuilders)
if err != nil {
return nil, nil, err
}
// Verify the source repository from the certificate.
if err := VerifyCertficateSourceRepository(workflowInfo, provenanceOpts.ExpectedSourceURI); err != nil {
return nil, nil, err
}
// Verify properties of the SLSA provenance.
// Unpack and verify info in the provenance, including the subject Digest.
provenanceOpts.ExpectedBuilderID = verifiedBuilderID.String()
// There is a corner-case to handle: if the verified builder ID from the cert
// is a delegator builder, the user MUST provide an expected builder ID
// and we MUST match it against the content of the provenance.
if err := VerifyProvenance(env, provenanceOpts, verifiedBuilderID, byob, builderOpts.ExpectedID); err != nil {
return nil, nil, err
}
if byob {
// Overwrite the builderID to match the one in the provenance.
verifiedBuilderID, err = builderID(env, verifiedBuilderID)
if err != nil {
return nil, nil, err
}
}
fmt.Fprintf(os.Stderr, "Verified build using builder %q at commit %s\n",
verifiedBuilderID.String(),
workflowInfo.SourceSha1)
// Return verified provenance.
r, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return nil, nil, err
}
return r, verifiedBuilderID, nil
}
func verifyNpmEnvAndCert(env *dsse.Envelope,
cert *x509.Certificate,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
defaultBuilders map[string]bool,
) (*utils.TrustedBuilderID, error) {
/* Verify properties of the signing identity. */
// Get the workflow info given the certificate information.
workflowInfo, err := GetWorkflowInfoFromCertificate(cert)
if err != nil {
return nil, err
}
// Verify the workflow identity.
// 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 + common.GenericLowPermsDelegatorBuilderID
delegatorBuilderOpts := options.BuilderOpts{
ExpectedID: &expectedDelegatorWorkflow,
}
trustedBuilderID, byob, 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) {
return nil, err
}
// Verify the source repository from the certificate.
if err := VerifyCertficateSourceRepository(workflowInfo, provenanceOpts.ExpectedSourceURI); err != nil {
return nil, err
}
// Users must always provide the builder ID.
if builderOpts == nil || builderOpts.ExpectedID == nil {
return nil, fmt.Errorf("%w: no expected builder ID", serrors.ErrorInvalidBuilderID)
}
// WARNING: builderID may be empty if it's not a trusted reusable builder workflow.
isTrustedBuilder := false
if trustedBuilderID != nil {
// 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.
if !byob {
return nil, fmt.Errorf("%w: byob is false", serrors.ErrorInternal)
}
provenanceOpts.ExpectedBuilderID = *builderOpts.ExpectedID
if workflowInfo.SubjectHosted != nil && *workflowInfo.SubjectHosted != HostedGitHub {
return nil, fmt.Errorf("%w: self hosted re-usable workflow", serrors.ErrorMismatchBuilderID)
}
isTrustedBuilder = true
} else {
// 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.
// TODO(https://github.com/gh-community/npm-provenance-private-beta-community/issues/9#issuecomment-1516685721):
// Allow the user to provide one of 3 builders: self-hosted, github-hosted and legacy github-hosted.
// Verify that the value provided is consistent with certificate information.
if workflowInfo.SubjectHosted == nil {
return nil, fmt.Errorf("%w: hosted status unknown", serrors.ErrorNotSupported)
}
switch *builderOpts.ExpectedID {
case common.NpmCLILegacyBuilderID, common.NpmCLIHostedBuilderID:
if *workflowInfo.SubjectHosted != HostedGitHub {
return nil, fmt.Errorf("%w: re-usable workflow is self-hosted", serrors.ErrorMismatchBuilderID)
}
case common.NpmCLISelfHostedBuilderID:
if *workflowInfo.SubjectHosted != HostedSelf {
return nil, fmt.Errorf("%w: re-usable workflow is GitHub-hosted", serrors.ErrorMismatchBuilderID)
}
default:
return nil, fmt.Errorf("%w: builder %v. Expected one of %v, %v", serrors.ErrorNotSupported, *builderOpts.ExpectedID,
common.NpmCLISelfHostedBuilderID, common.NpmCLIHostedBuilderID)
}
trustedBuilderID, err = utils.TrustedBuilderIDNew(*builderOpts.ExpectedID, false)
if err != nil {
return nil, err
}
// On GitHub we only support the default GitHub runner builder.
provenanceOpts.ExpectedBuilderID = *builderOpts.ExpectedID
}
// Verify properties of the SLSA provenance.
// Unpack and verify info in the provenance, including the Subject Digest.
if err := VerifyNpmPackageProvenance(env, workflowInfo, provenanceOpts, trustedBuilderID, isTrustedBuilder); err != nil {
return nil, err
}
fmt.Fprintf(os.Stderr, "Verified build using builder %s at commit %s\n",
trustedBuilderID.String(),
workflowInfo.SourceSha1)
return trustedBuilderID, nil
}
// VerifyArtifact verifies provenance for an artifact.
func (v *GHAVerifier) VerifyArtifact(ctx context.Context,
provenance []byte, artifactHash string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.TrustedBuilderID, error) {
isSigstoreBundle := IsSigstoreBundle(provenance)
// This includes a default retry count of 3.
rClient, err := getDefaultRekorClient()
if err != nil {
return nil, nil, err
}
trustedRoot, err := utils.GetSigstoreTrustedRoot()
if err != nil {
return nil, nil, err
}
var signedAtt *SignedAttestation
/* Verify signature on the intoto attestation. */
if isSigstoreBundle {
signedAtt, err = VerifyProvenanceBundle(ctx, provenance, trustedRoot)
} else {
signedAtt, err = VerifyProvenanceSignature(ctx, trustedRoot, rClient,
provenance, artifactHash)
}
if err != nil {
return nil, nil, err
}
return verifyEnvAndCert(signedAtt.Envelope, signedAtt.SigningCert,
provenanceOpts, builderOpts,
utils.MergeMaps(defaultArtifactTrustedReusableWorkflows, defaultBYOBReusableWorkflows))
}
// VerifyGithubAttestation verifies provenance for a Github Attestations.
func (v *GHAVerifier) VerifyGithubAttestation(ctx context.Context,
attestation []byte,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.TrustedBuilderID, error) {
if !IsSigstoreBundle(attestation) {
return nil, nil, errors.New("github attestations must be signed by Sigstore")
}
trustedRoot, err := utils.GetSigstoreTrustedRoot()
if err != nil {
return nil, nil, err
}
/* Verify signature on the intoto attestation. */
signedAtt, err := VerifyProvenanceBundle(ctx, attestation, trustedRoot)
if err != nil {
return nil, nil, err
}
return verifyEnvAndCert(signedAtt.Envelope, signedAtt.SigningCert,
provenanceOpts, builderOpts, map[string]bool{})
}
// VerifyImage verifies provenance for an OCI image.
func (v *GHAVerifier) VerifyImage(ctx context.Context,
provenance []byte, artifactImage string, provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.TrustedBuilderID, error) {
var provenanceTargetRepository name.Repository
var err error
// Consume input for --provenance-repository when set
if provenanceOpts.ExpectedProvenanceRepository != nil {
provenanceTargetRepository, err = name.NewRepository(*provenanceOpts.ExpectedProvenanceRepository)
if err != nil {
return nil, nil, err
}
}
registryClientOpts := []ociremote.Option{}
// Append target repository to OCI Registry opts
// Must be authenticated against the specified target repository externally
if provenanceTargetRepository.Name() != "" {
registryClientOpts = append(registryClientOpts, ociremote.WithTargetRepository(provenanceTargetRepository))
}
/* Retrieve any valid signed attestations that chain up to Fulcio root CA. */
opts, err := getDefaultCosignCheckOpts(ctx)
if err != nil {
return nil, nil, err
}
opts.RegistryClientOpts = registryClientOpts
atts, _, err := container.RunCosignImageVerification(ctx,
artifactImage, opts)
if err != nil {
return nil, nil, err
}
/* Now verify properties of the attestations */
var errs []error
var builderID *utils.TrustedBuilderID
var verifiedProvenance []byte
for _, att := range atts {
pyld, err := att.Payload()
if err != nil {
fmt.Fprintf(os.Stderr, "unexpected error getting payload from OCI registry %s", err)
continue
}
env, err := EnvelopeFromBytes(pyld)
if err != nil {
fmt.Fprintf(os.Stderr, "unexpected error parsing envelope from OCI registry %s", err)
continue
}
cert, err := att.Cert()
if err != nil {
fmt.Fprintf(os.Stderr, "unexpected error getting certificate from OCI registry %s", err)
continue
}
verifiedProvenance, builderID, err = verifyEnvAndCert(env,
cert, provenanceOpts, builderOpts,
defaultContainerTrustedReusableWorkflows)
if err == nil {
return verifiedProvenance, builderID, nil
}
errs = append(errs, err)
}
// Return the first error.
if len(errs) > 0 {
var s string
if len(errs) > 1 {
s = fmt.Sprintf(": %v", errs[1:])
}
return nil, nil, fmt.Errorf("%w%s", errs[0], s)
}
return nil, nil, fmt.Errorf("%w", serrors.ErrorNoValidSignature)
}
// VerifyNpmPackage verifies an npm package tarball.
func (v *GHAVerifier) VerifyNpmPackage(ctx context.Context,
attestations []byte, tarballHash string,
provenanceOpts *options.ProvenanceOpts,
builderOpts *options.BuilderOpts,
) ([]byte, *utils.TrustedBuilderID, error) {
trustedRoot, err := utils.GetSigstoreTrustedRoot()
if err != nil {
return nil, nil, err
}
npm, err := NpmNew(ctx, trustedRoot, attestations)
if err != nil {
return nil, nil, err
}
// Verify provenance signature.
if err := npm.verifyProvenanceAttestationSignature(); err != nil {
return nil, nil, err
}
// Verify provenance builder information.
builder, err := npm.verifyBuilderID(
provenanceOpts, builderOpts,
defaultBYOBReusableWorkflows)
if err != nil {
return nil, nil, err
}
// Verify publish attesttation signature.
if err := npm.verifyPublishAttestationSignature(); err != nil {
return nil, nil, err
}
// Verify publish subject digest.
if err := npm.verifyPublishAttestationSubjectDigest(provenanceOpts.ExpectedDigest); err != nil {
return nil, nil, err
}
// Verify attestation headers.
if err := npm.verifyIntotoHeaders(); err != nil {
return nil, nil, err
}
// Verify package names match.
if provenanceOpts != nil {
if err := npm.verifyPackageName(provenanceOpts.ExpectedPackageName); err != nil {
return nil, nil, err
}
if err := npm.verifyPackageVersion(provenanceOpts.ExpectedPackageVersion); err != nil {
return nil, nil, err
}
}
prov, err := npm.verifiedProvenanceBytes()
if err != nil {
return nil, nil, err
}
return prov, builder, nil
}