Files
slsa-verifier/pkg/provenance.go
asraa 3a059ae446 fix: add verification without redis index (#97)
* add verification without redis index

Signed-off-by: Asra Ali <asraa@google.com>
2022-06-20 15:05:20 -05:00

863 lines
26 KiB
Go

package pkg
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
"golang.org/x/mod/semver"
cjson "github.com/docker/go/canonical/json"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/google/trillian/merkle/logverifier"
"github.com/google/trillian/merkle/rfc6962"
intoto "github.com/in-toto/in-toto-golang/in_toto"
dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/dsse"
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/client/entries"
"github.com/sigstore/rekor/pkg/generated/client/index"
"github.com/sigstore/rekor/pkg/generated/client/tlog"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/rekor/pkg/types"
intotod "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
"github.com/sigstore/rekor/pkg/util"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/slsa-framework/slsa-github-generator/signing/envelope"
)
const (
defaultRekorAddr = "https://rekor.sigstore.dev"
certOidcIssuer = "https://token.actions.githubusercontent.com"
)
var (
trustedBuilderRepository = "slsa-framework/slsa-github-generator"
e2eTestRepository = "slsa-framework/example-package"
)
var trustedReusableWorkflows = map[string]bool{
trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml": true,
trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml": true,
}
var (
ErrorInvalidDssePayload = errors.New("invalid DSSE envelope payload")
ErrorMismatchBranch = errors.New("branch used to generate the binary does not match provenance")
ErrorMismatchRepository = errors.New("repository used to generate the binary does not match provenance")
ErrorMismatchTag = errors.New("tag used to generate the binary does not match provenance")
ErrorMismatchVersionedTag = errors.New("tag used to generate the binary does not match provenance")
ErrorInvalidSemver = errors.New("invalid semantic version")
ErrorRekorSearch = errors.New("error searching rekor entries")
errorMismatchHash = errors.New("binary artifact hash does not match provenance subject")
errorInvalidVersion = errors.New("invalid version")
errorInvalidRef = errors.New("invalid ref")
errorMalformedWorkflowURI = errors.New("malformed URI for workflow")
ErrorUntrustedReusableWorkflow = errors.New("untrusted reusable workflow")
ErrorNoValidRekorEntries = errors.New("could not find a matching valid signature entry")
)
func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) {
env = &dsselib.Envelope{}
err = json.Unmarshal(payload, env)
return
}
func intotoEntry(certPem []byte, provenance []byte) (*intotod.V001Entry, error) {
cert := strfmt.Base64(certPem)
return &intotod.V001Entry{
IntotoObj: models.IntotoV001Schema{
Content: &models.IntotoV001SchemaContent{
Envelope: string(provenance),
},
PublicKey: &cert,
},
}, nil
}
// Get SHA256 Subject Digest from the provenance statement.
func getSha256Digest(env *dsselib.Envelope) (string, error) {
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "decoding payload")
}
prov := &intoto.ProvenanceStatement{}
if err := json.Unmarshal([]byte(pyld), prov); err != nil {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "unmarshalling json")
}
if len(prov.Subject) == 0 {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "no subjects")
}
digestSet := prov.Subject[0].Digest
hash, exists := digestSet["sha256"]
if !exists {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "no sha256 subject digest")
}
return hash, nil
}
// GetRekorEntries finds all entry UUIDs by the digest of the artifact binary.
func GetRekorEntries(rClient *client.Rekor, artifactHash string) ([]string, error) {
// Use search index to find rekor entry UUIDs that match Subject Digest.
params := index.NewSearchIndexParams()
params.Query = &models.SearchIndex{Hash: fmt.Sprintf("sha256:%v", artifactHash)}
resp, err := rClient.Index.SearchIndex(params)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrorRekorSearch, err.Error())
}
if len(resp.Payload) == 0 {
return nil, fmt.Errorf("%w: no matching entries found", ErrorRekorSearch)
}
return resp.GetPayload(), nil
}
// GetRekorEntriesWithCert finds all entry UUIDs with the full intoto attestation.
// The attestation generated by the slsa-github-generator libraries contain a signing certificate.
func GetRekorEntriesWithCert(rClient *client.Rekor, artifactHash string, provenance []byte) (*dsselib.Envelope, *x509.Certificate, error) {
// Use intoto attestation to find rekor entry UUIDs.
params := entries.NewSearchLogQueryParams()
searchLogQuery := models.SearchLogQuery{}
certPem, err := envelope.GetCertFromEnvelope([]byte(provenance))
if err != nil {
return nil, nil, fmt.Errorf("error getting certificate from provenance: %w", err)
}
e, err := intotoEntry(certPem, provenance)
if err != nil {
return nil, nil, fmt.Errorf("error creating intoto entry: %w", err)
}
entry := models.Intoto{
APIVersion: swag.String(e.APIVersion()),
Spec: e.IntotoObj,
}
entries := []models.ProposedEntry{&entry}
searchLogQuery.SetEntries(entries)
params.SetEntry(&searchLogQuery)
resp, err := rClient.Entries.SearchLogQuery(params)
if err != nil {
return nil, nil, fmt.Errorf("%w: %s", ErrorRekorSearch, err.Error())
}
if len(resp.GetPayload()) != 1 {
return nil, nil, fmt.Errorf("%w: %s", ErrorRekorSearch, "no matching rekor entries")
}
logEntry := resp.Payload[0]
for uuid, e := range logEntry {
if _, err := verifyTlogEntry(context.Background(), rClient, uuid, e); err != nil {
return nil, nil, fmt.Errorf("error verifying tlog entry: %w", err)
}
url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", uuid)
fmt.Fprintf(os.Stderr, "Verified signature against tlog entry index %d at URL: %s\n", *e.LogIndex, url)
}
env, err := EnvelopeFromBytes(provenance)
if err != nil {
return nil, nil, err
}
certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPem)
if err != nil {
return nil, nil, err
}
if len(certs) != 1 {
return nil, nil, fmt.Errorf("error unmarshaling certificate from pem")
}
return env, certs[0], nil
}
func verifyRootHash(ctx context.Context, rekorClient *client.Rekor, proof *models.InclusionProof, pub *ecdsa.PublicKey) error {
infoParams := tlog.NewGetLogInfoParamsWithContext(ctx)
result, err := rekorClient.Tlog.GetLogInfo(infoParams)
if err != nil {
return err
}
logInfo := result.GetPayload()
sth := util.SignedCheckpoint{}
if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil {
return err
}
verifier, err := signature.LoadVerifier(pub, crypto.SHA256)
if err != nil {
return err
}
if !sth.Verify(verifier) {
return errors.New("signature on tree head did not verify")
}
rootHash, err := hex.DecodeString(*proof.RootHash)
if err != nil {
return errors.New("error decoding root hash in inclusion proof")
}
if *proof.TreeSize == int64(sth.Size) {
if !bytes.Equal(rootHash, sth.Hash) {
return errors.New("root hash returned from server does not match inclusion proof hash")
}
} else if *proof.TreeSize < int64(sth.Size) {
consistencyParams := tlog.NewGetLogProofParamsWithContext(ctx)
consistencyParams.FirstSize = proof.TreeSize // Root hash at the time the proof was returned
consistencyParams.LastSize = int64(sth.Size) // Root hash verified with rekor pubkey
consistencyProof, err := rekorClient.Tlog.GetLogProof(consistencyParams)
if err != nil {
return err
}
hashes := [][]byte{}
for _, h := range consistencyProof.Payload.Hashes {
b, err := hex.DecodeString(h)
if err != nil {
return errors.New("error decoding consistency proof hashes")
}
hashes = append(hashes, b)
}
v := logverifier.New(rfc6962.DefaultHasher)
if err := v.VerifyConsistencyProof(*proof.TreeSize, int64(sth.Size), rootHash, sth.Hash, hashes); err != nil {
return err
}
} else if *proof.TreeSize > int64(sth.Size) {
return errors.New("inclusion proof returned a tree size larger than the verified tree size")
}
return nil
}
func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, uuid string) (*models.LogEntryAnon, error) {
params := entries.NewGetLogEntryByUUIDParamsWithContext(ctx)
params.EntryUUID = uuid
lep, err := rekorClient.Entries.GetLogEntryByUUID(params)
if err != nil {
return nil, err
}
if len(lep.Payload) != 1 {
return nil, errors.New("UUID value can not be extracted")
}
e := lep.Payload[params.EntryUUID]
return verifyTlogEntry(ctx, rekorClient, params.EntryUUID, e)
}
func verifyTlogEntry(ctx context.Context, rekorClient *client.Rekor, uuid string, e models.LogEntryAnon) (*models.LogEntryAnon, error) {
if e.Verification == nil || e.Verification.InclusionProof == nil {
return nil, errors.New("inclusion proof not provided")
}
hashes := [][]byte{}
for _, h := range e.Verification.InclusionProof.Hashes {
hb, err := hex.DecodeString(h)
if err != nil {
return nil, errors.New("error decoding inclusion proof hashes")
}
hashes = append(hashes, hb)
}
rootHash, err := hex.DecodeString(*e.Verification.InclusionProof.RootHash)
if err != nil {
return nil, errors.New("error decoding hex encoded root hash")
}
leafHash, err := hex.DecodeString(uuid)
if err != nil {
return nil, errors.New("error decoding hex encoded leaf hash")
}
// Verify the root hash against the current Signed Entry Tree Head
pubs, err := cosign.GetRekorPubs(ctx, nil)
if err != nil {
return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository")
}
var entryVerError error
for _, pubKey := range pubs {
// Verify inclusion against the signed tree head
entryVerError = verifyRootHash(ctx, rekorClient, e.Verification.InclusionProof, pubKey.PubKey)
if entryVerError == nil {
break
}
}
if entryVerError != nil {
return nil, fmt.Errorf("%w: %s", err, "error verifying root hash")
}
// Verify the entry's inclusion
v := logverifier.New(rfc6962.DefaultHasher)
if err := v.VerifyInclusionProof(*e.Verification.InclusionProof.LogIndex, *e.Verification.InclusionProof.TreeSize, hashes, rootHash, leafHash); err != nil {
return nil, fmt.Errorf("%w: %s", err, "verifying inclusion proof")
}
// Verify rekor's signature over the SET.
payload := bundle.RekorPayload{
Body: e.Body,
IntegratedTime: *e.IntegratedTime,
LogIndex: *e.LogIndex,
LogID: *e.LogID,
}
var setVerError error
for _, pubKey := range pubs {
setVerError = cosign.VerifySET(payload, []byte(e.Verification.SignedEntryTimestamp), pubKey.PubKey)
// Return once the SET is verified successfully.
if setVerError == nil {
break
}
}
return &e, setVerError
}
func extractCert(e *models.LogEntryAnon) (*x509.Certificate, error) {
b, err := base64.StdEncoding.DecodeString(e.Body.(string))
if err != nil {
return nil, err
}
pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), runtime.JSONConsumer())
if err != nil {
return nil, err
}
eimpl, err := types.NewEntry(pe)
if err != nil {
return nil, err
}
var publicKeyB64 []byte
switch e := eimpl.(type) {
case *intotod.V001Entry:
publicKeyB64, err = e.IntotoObj.PublicKey.MarshalText()
if err != nil {
return nil, err
}
default:
return nil, errors.New("unexpected tlog entry type")
}
publicKey, err := base64.StdEncoding.DecodeString(string(publicKeyB64))
if err != nil {
return nil, err
}
certs, err := cryptoutils.UnmarshalCertificatesFromPEM(publicKey)
if err != nil {
return nil, err
}
if len(certs) != 1 {
return nil, errors.New("unexpected number of cert pem tlog entry")
}
return certs[0], err
}
// VerifyProvenanceSignature returns the verified DSSE envelope containing the provenance
// and the signing certificate given the provenance and artifact hash.
func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor, provenance []byte, artifactHash string) (*dsselib.Envelope, *x509.Certificate, error) {
// Get Rekor entries corresponding to provenance
if env, cert, err := GetRekorEntriesWithCert(rClient, artifactHash, provenance); err == nil {
return env, cert, nil
}
// Fallback on using the redis search index to get matching UUIDs.
uuids, err := GetRekorEntries(rClient, artifactHash)
if err != nil {
return nil, nil, err
}
env, err := EnvelopeFromBytes(provenance)
if err != nil {
return nil, nil, err
}
// Verify the provenance and return the signing certificate.
cert, err := FindSigningCertificate(ctx, uuids, *env, rClient)
if err != nil {
return nil, nil, err
}
return env, cert, nil
}
// FindSigningCertificate finds and verifies a matching signing certificate from a list of Rekor entry UUIDs.
func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dsselib.Envelope, rClient *client.Rekor) (*x509.Certificate, error) {
attBytes, err := cjson.MarshalCanonical(dssePayload)
if err != nil {
return nil, err
}
// Iterate through each matching UUID and perform:
// * Verify TLOG entry (inclusion and signed entry timestamp against Rekor pubkey).
// * Verify the signing certificate against the Fulcio root CA.
// * Verify dsse envelope signature against signing certificate.
// * Check signature expiration against IntegratedTime in entry.
// * If all succeed, return the signing certificate.
for _, uuid := range uuids {
entry, err := verifyTlogEntryByUUID(ctx, rClient, uuid)
if err != nil {
continue
}
cert, err := extractCert(entry)
if err != nil {
continue
}
roots, err := fulcio.GetRoots()
if err != nil {
continue
}
co := &cosign.CheckOpts{
RootCerts: roots,
CertOidcIssuer: certOidcIssuer,
}
verifier, err := cosign.ValidateAndUnpackCert(cert, co)
if err != nil {
continue
}
verifier = dsse.WrapVerifier(verifier)
if err := verifier.VerifySignature(bytes.NewReader(attBytes), bytes.NewReader(attBytes)); err != nil {
continue
}
it := time.Unix(*entry.IntegratedTime, 0)
if err := cosign.CheckExpiry(cert, it); err != nil {
continue
}
uuid, err := cosign.ComputeLeafHash(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Error computing leaf hash for tlog entry at index: %d\n", *entry.LogIndex)
continue
}
// success!
url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", hex.EncodeToString(uuid))
fmt.Fprintf(os.Stderr, "Verified signature against tlog entry index %d at URL: %s\n", *entry.LogIndex, url)
return cert, nil
}
return nil, ErrorNoValidRekorEntries
}
func getExtension(cert *x509.Certificate, oid string) string {
for _, ext := range cert.Extensions {
if strings.Contains(ext.Id.String(), oid) {
return string(ext.Value)
}
}
return ""
}
type WorkflowIdentity struct {
// The caller repository
CallerRepository string `json:"caller"`
// The commit SHA where the workflow was triggered
CallerHash string `json:"commit"`
// Current workflow (reuseable workflow) ref
JobWobWorkflowRef string `json:"job_workflow_ref"`
// Trigger
Trigger string `json:"trigger"`
// Issuer
Issuer string `json:"issuer"`
}
// GetWorkflowFromCertificate gets the workflow identity from the Fulcio authenticated content.
func GetWorkflowInfoFromCertificate(cert *x509.Certificate) (*WorkflowIdentity, error) {
if len(cert.URIs) == 0 {
return nil, errors.New("missing URI information from certificate")
}
return &WorkflowIdentity{
CallerRepository: getExtension(cert, "1.3.6.1.4.1.57264.1.5"),
Issuer: getExtension(cert, "1.3.6.1.4.1.57264.1.1"),
Trigger: getExtension(cert, "1.3.6.1.4.1.57264.1.2"),
CallerHash: getExtension(cert, "1.3.6.1.4.1.57264.1.3"),
JobWobWorkflowRef: cert.URIs[0].Path,
}, nil
}
// VerifyWorkflowIdentity verifies the signing certificate information
func VerifyWorkflowIdentity(id *WorkflowIdentity, source string) error {
// cert URI path is /org/repo/path/to/workflow@ref
workflowPath := strings.SplitN(id.JobWobWorkflowRef, "@", 2)
if len(workflowPath) < 2 {
return fmt.Errorf("%w: %s", errorMalformedWorkflowURI, id.JobWobWorkflowRef)
}
// Trusted workflow verification by name.
reusableWorkflowName := strings.Trim(workflowPath[0], "/")
if _, ok := trustedReusableWorkflows[reusableWorkflowName]; !ok {
return fmt.Errorf("%w: %s", ErrorUntrustedReusableWorkflow, reusableWorkflowName)
}
// Verify the ref.
if err := verifyTrustedBuilderRef(id, strings.Trim(workflowPath[1], "/")); err != nil {
return err
}
// Issue verification.
if !strings.EqualFold(id.Issuer, certOidcIssuer) {
return fmt.Errorf("untrusted token issuer: %s", id.Issuer)
}
// The caller repository in the x509 extension is not fully qualified. It only contains
// {org}/{repository}.
expectedSource := strings.TrimPrefix(source, "github.com/")
if !strings.EqualFold(id.CallerRepository, expectedSource) {
return fmt.Errorf("%w: expected source '%s', got '%s'", ErrorMismatchRepository,
expectedSource, id.CallerRepository)
}
return nil
}
// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
// This lets us use the pre-build builder binary generated during release (release happen at main).
// For other projects, we only allow semantic versions that map to a release.
func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
if (id.CallerRepository == trustedBuilderRepository ||
id.CallerRepository == e2eTestRepository) &&
strings.EqualFold("refs/heads/main", ref) {
return nil
}
if !strings.HasPrefix(ref, "refs/tags/") {
return fmt.Errorf("%w: %s: not of the form 'refs/tags/name'", errorInvalidRef, ref)
}
// Valid semver of the form vX.Y.Z with no metadata.
pin := strings.TrimPrefix(ref, "refs/tags/")
if !(semver.IsValid(pin) &&
len(strings.Split(pin, ".")) == 3 &&
semver.Prerelease(pin) == "" &&
semver.Build(pin) == "") {
return fmt.Errorf("%w: %s: not of the form vX.Y.Z", errorInvalidRef, pin)
}
return nil
}
func VerifyProvenance(env *dsselib.Envelope, expectedHash string) error {
hash, err := getSha256Digest(env)
if err != nil {
return err
}
if !strings.EqualFold(hash, expectedHash) {
return fmt.Errorf("expected hash '%s', got '%s': %w", expectedHash, hash, errorMismatchHash)
}
return nil
}
func VerifyBranch(env *dsselib.Envelope, expectedBranch string) error {
branch, err := getBranch(env)
if err != nil {
return err
}
expectedBranch = "refs/heads/" + expectedBranch
if !strings.EqualFold(branch, expectedBranch) {
return fmt.Errorf("expected branch '%s', got '%s': %w", expectedBranch, branch, ErrorMismatchBranch)
}
return nil
}
func VerifyTag(env *dsselib.Envelope, expectedTag string) error {
tag, err := getTag(env)
if err != nil {
return err
}
expectedTag = "refs/tags/" + expectedTag
if !strings.EqualFold(tag, expectedTag) {
return fmt.Errorf("expected tag '%s', got '%s': %w", expectedTag, tag, ErrorMismatchTag)
}
return nil
}
func VerifyVersionedTag(env *dsselib.Envelope, expectedTag string) error {
// Validate and canonicalize the provenance tag.
if !semver.IsValid(expectedTag) {
return fmt.Errorf("%s: %w", expectedTag, ErrorInvalidSemver)
}
// Retrieve, validate and canonicalize the provenance tag.
// Note: prerelease is validated as part of patch validation
// and must be equal. Build is discarded as per https://semver.org/:
// "Build metadata MUST be ignored when determining version precedence",
tag, err := getTag(env)
if err != nil {
return err
}
semTag := semver.Canonical(strings.TrimPrefix(tag, "refs/tags/"))
if !semver.IsValid(semTag) {
return fmt.Errorf("%s: %w", expectedTag, ErrorInvalidSemver)
}
// Major should always be the same.
expectedMajor := semver.Major(expectedTag)
major := semver.Major(semTag)
if major != expectedMajor {
return fmt.Errorf("%w: major version expected '%s', got '%s'",
ErrorMismatchVersionedTag, expectedMajor, major)
}
expectedMinor, err := minorVersion(expectedTag)
if err == nil {
// A minor version was provided by the user.
minor, err := minorVersion(semTag)
if err != nil {
return err
}
if minor != expectedMinor {
return fmt.Errorf("%w: minor version expected '%s', got '%s'",
ErrorMismatchVersionedTag, expectedMinor, minor)
}
}
expectedPatch, err := patchVersion(expectedTag)
if err == nil {
// A patch version was provided by the user.
patch, err := patchVersion(semTag)
if err != nil {
return err
}
if patch != expectedPatch {
return fmt.Errorf("%w: patch version expected '%s', got '%s'",
ErrorMismatchVersionedTag, expectedPatch, patch)
}
}
// Match.
return nil
}
func minorVersion(v string) (string, error) {
return extractFromVersion(v, 1)
}
func patchVersion(v string) (string, error) {
patch, err := extractFromVersion(v, 2)
if err != nil {
return "", err
}
return strings.TrimSuffix(patch, semver.Build(v)), nil
}
func extractFromVersion(v string, i int) (string, error) {
parts := strings.Split(v, ".")
if len(parts) <= i {
return "", fmt.Errorf("%s: %w", v, ErrorInvalidSemver)
}
return parts[i], nil
}
func getAsInt(environment map[string]interface{}, field string) (int, error) {
value, ok := environment[field]
if !ok {
return -1, fmt.Errorf("%w: %s", ErrorInvalidDssePayload,
fmt.Sprintf("environment type for %s", field))
}
i, ok := value.(float64)
if !ok {
return -1, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "environment type float64")
}
return int(i), nil
}
func getAsString(environment map[string]interface{}, field string) (string, error) {
value, ok := environment[field]
if !ok {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload,
fmt.Sprintf("environment type for %s", field))
}
i, ok := value.(string)
if !ok {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "environment type string")
}
return i, nil
}
func getEventPayload(environment map[string]interface{}) (map[string]interface{}, error) {
eventPayload, ok := environment["github_event_payload"]
if !ok {
return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type event payload")
}
payload, ok := eventPayload.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type payload")
}
return payload, nil
}
func getBaseRef(environment map[string]interface{}) (string, error) {
baseRef, err := getAsString(environment, "github_base_ref")
if err != nil {
return "", err
}
// This `base_ref` seems to always be "".
if baseRef != "" {
return baseRef, nil
}
// Look at the event payload instead.
// We don't do that for all triggers because the payload
// is event-specific; and only the `push` event seems to have a `base_ref`.
eventName, err := getAsString(environment, "github_event_name")
if err != nil {
return "", err
}
if eventName != "push" {
return "", nil
}
payload, err := getEventPayload(environment)
if err != nil {
return "", err
}
return getAsString(payload, "base_ref")
}
func getTargetCommittish(environment map[string]interface{}) (string, error) {
eventName, err := getAsString(environment, "github_event_name")
if err != nil {
return "", err
}
if eventName != "release" {
return "", nil
}
payload, err := getEventPayload(environment)
if err != nil {
return "", err
}
// For a release event, we look for release.target_commitish.
releasePayload, ok := payload["release"]
if !ok {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "release absent from payload")
}
release, ok := releasePayload.(map[string]interface{})
if !ok {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type releasePayload")
}
branch, err := getAsString(release, "target_commitish")
if err != nil {
return "", fmt.Errorf("%w: %s", err, "target_commitish not present")
}
return "refs/heads/" + branch, nil
}
func getBranchForTag(environment map[string]interface{}) (string, error) {
// First try the base_ref.
branch, err := getBaseRef(environment)
if branch != "" || err != nil {
return branch, err
}
// Second try the target comittish.
return getTargetCommittish(environment)
}
// Get tag from the provenance invocation parameters.
func getTag(env *dsselib.Envelope) (string, error) {
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "decoding payload")
}
var prov intoto.ProvenanceStatement
if err := json.Unmarshal([]byte(pyld), &prov); err != nil {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "unmarshalling json")
}
environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{})
if !ok {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type")
}
refType, err := getAsString(environment, "github_ref_type")
if err != nil {
return "", err
}
switch refType {
case "branch":
return "", nil
case "tag":
return getAsString(environment, "github_ref")
default:
return "", fmt.Errorf("%w: %s %s", ErrorInvalidDssePayload,
"unknown ref type", refType)
}
}
// Get branch from the provenance invocation parameters.
func getBranch(env *dsselib.Envelope) (string, error) {
pyld, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "decoding payload")
}
var prov intoto.ProvenanceStatement
if err := json.Unmarshal([]byte(pyld), &prov); err != nil {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "unmarshalling json")
}
environment, ok := prov.Predicate.Invocation.Environment.(map[string]interface{})
if !ok {
return "", fmt.Errorf("%w: %s", ErrorInvalidDssePayload, "parameters type")
}
refType, err := getAsString(environment, "github_ref_type")
if err != nil {
return "", err
}
switch refType {
case "branch":
return getAsString(environment, "github_ref")
case "tag":
return getBranchForTag(environment)
default:
return "", fmt.Errorf("%w: %s %s", ErrorInvalidDssePayload,
"unknown ref type", refType)
}
}