diff --git a/cmd/kosli/attest.go b/cmd/kosli/attest.go index 90af8ba9f..d55dc2d3a 100644 --- a/cmd/kosli/attest.go +++ b/cmd/kosli/attest.go @@ -26,6 +26,7 @@ func newAttestCmd(out io.Writer) *cobra.Command { newAttestSonarCmd(out), newAttestCustomCmd(out), newAttestSbomCmd(out), + newAttestInTotoCmd(out), newAttestOverrideCmd(out), newAttestDecisionCmd(out), ) diff --git a/cmd/kosli/attestInToto.go b/cmd/kosli/attestInToto.go new file mode 100644 index 000000000..478fc81d2 --- /dev/null +++ b/cmd/kosli/attestInToto.go @@ -0,0 +1,552 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + ghUtils "github.com/kosli-dev/cli/internal/github" + "github.com/kosli-dev/cli/internal/gitview" + "github.com/kosli-dev/cli/internal/intoto" + "github.com/kosli-dev/cli/internal/requests" + "github.com/kosli-dev/cli/internal/types" + "github.com/spf13/cobra" +) + +// Annotations attest in-toto derives from the attestation. A caller cannot +// set them with --annotate. +const ( + builderIDAnnotation = "builder_id" + buildTypeAnnotation = "build_type" + workflowAnnotation = "workflow" + verifiedAnnotation = "verified" + signerIdentityAnnotation = "signer_identity" + signerIssuerAnnotation = "signer_issuer" + logIndexAnnotation = "log_index" + integratedTimeAnnotation = "integrated_time" + signedTimestampAnnotation = "signed_timestamp" +) + +// attestationExternalURL labels the link to the attestation's transparency +// log entry. +const attestationExternalURL = "attestation" + +// templateArtifactNamePattern is what the API accepts as an artifact's name +// in a flow template. +var templateArtifactNamePattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +// intotoTrustRoots supplies the Sigstore trust roots signatures are checked +// against. Tests replace it so that no test fetches one over the network. +var intotoTrustRoots = intoto.SigstoreTrustRoots + +const intotoSourceBundle = "bundle" + +type attestInTotoOptions struct { + fingerprintOptions *fingerprintOptions + fingerprint string + flowName string + trailName string + name string + source string + bundlePath string + predicateType string + kind intoto.PredicateKind + policy intoto.Policy + buildURL string + commitURL string + repoProvider string + repoRoot string + githubToken string + githubBaseURL string + annotations map[string]string + externalURLs map[string]string + externalFingerprints map[string]string +} + +const attestInTotoShortDesc = `Record an in-toto attestation, such as SLSA provenance or an SBOM, that a build already produced. ` + +const attestInTotoLongDesc = attestInTotoShortDesc + ` +Build systems produce in-toto attestations as they build: ^actions/attest^, ^cosign attest^ +and ^docker buildx --provenance --sbom^ among them. This command records one in Kosli, +verifying its signature first, rather than regenerating the same evidence with other +^kosli attest^ commands. + +^--predicate-type^ chooses what to record, one kind per command: + +- ^provenance^ (SLSA provenance v1 or v0.2) reports the artifact named by ^--name^, as + ^kosli attest artifact^ does. The commit, branch, repository and build URL come from the + statement, never from the CI environment, so the build's own commit and run are recorded + even when this command runs in a different job or workflow. +- ^sbom^ (SPDX or CycloneDX) reports an SBOM attestation named by ^--name^, as + ^kosli attest sbom^ does, with the attestation attached in place of the SBOM file. + +Recording provenance and an SBOM takes two commands. + +The attestation is read from the file given with ^--bundle^: a Sigstore bundle +(^.sigstore.json^), a DSSE envelope, an in-toto statement, or an ^.intoto.jsonl^ file with +one of these per line. Attestations of other kinds are ignored. When several qualify, a +verified one is preferred, then the newest. + +The statement must list the artifact as its subject. A signed attestation is verified +with Sigstore against a signer you pin: ^--certificate-identity^ (or +^--certificate-identity-regexp^) with ^--certificate-oidc-issuer^, or ^--key^. There is no +way to accept any valid certificate. The trust roots of Sigstore's public-good instance and +of GitHub's own instance, which signs attestations for private repositories, are fetched +over TUF. An unsigned attestation is refused unless ^--allow-unsigned^ is given, and is then +recorded as unverified. An attestation that fails verification, because another signer made +it or it was changed after signing, fails the command and nothing is recorded, even when +another attestation in the file would qualify. So does a signed attestation whose signature +cannot be checked, such as when a trust root cannot be fetched or the signer flags do not fit +how it was signed; the error then says it could not be verified, not that it failed. + +The record is annotated with ^verified^ and, for a signed attestation, ^signer_identity^, +^signer_issuer^, ^log_index^, ^integrated_time^ and ^signed_timestamp^ where they apply. +Provenance adds ^builder_id^, ^build_type^ and ^workflow^, and an SBOM adds ^sbom_format^ +and ^sbom_sha256^. A transparency log entry is linked as the ^attestation^ external URL, +pinned by the SHA-256 of the attestation's bytes. + +For provenance, the commit's message, author and time are looked up on GitHub with +^--github-token^ when the statement names a GitHub repository, or else read from the git +checkout at ^--repo-root^ if it holds the commit. Failing both, only the commit's SHA is +recorded, without its details or branch. A repository on GitHub Enterprise Server needs +^--repo-provider github^ and ^--github-base-url^ to be looked up there: the token is only +sent to the server you name, never to a host taken from the statement. A build URL or +commit URL the statement does not carry can be given with ^--build-url^ and ^--commit-url^. +` + fingerprintDesc + kosliIgnoreDesc + +const attestInTotoExample = ` +# record verified SLSA provenance from a Sigstore bundle, as the artifact named app: +kosli attest in-toto app.tar.gz \ + --artifact-type file \ + --bundle app.sigstore.json \ + --predicate-type provenance \ + --name app \ + --certificate-identity https://github.com/yourOrg/yourRepo/.github/workflows/build.yml@refs/heads/main \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --github-token yourGithubToken \ + --flow yourFlowName \ + --trail yourTrailName \ + --api-token yourAPIToken \ + --org yourOrgName + +# record the SBOM attestation from the same build, as app.sbom: +kosli attest in-toto app.tar.gz \ + --artifact-type file \ + --bundle app-sbom.sigstore.json \ + --predicate-type sbom \ + --name app.sbom \ + --certificate-identity-regexp '^https://github.com/yourOrg/yourRepo/' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --flow yourFlowName \ + --trail yourTrailName \ + --api-token yourAPIToken \ + --org yourOrgName + +# record unsigned provenance written by docker buildx, marked as unverified: +kosli attest in-toto yourImageName \ + --artifact-type docker \ + --bundle provenance.json \ + --predicate-type provenance \ + --name app \ + --allow-unsigned \ + --build-url https://ci.example.com/builds/123 \ + --flow yourFlowName \ + --trail yourTrailName \ + --api-token yourAPIToken \ + --org yourOrgName +` + +func newAttestInTotoCmd(out io.Writer) *cobra.Command { + o := &attestInTotoOptions{fingerprintOptions: &fingerprintOptions{}} + cmd := &cobra.Command{ + // Args: cobra.MaximumNArgs(1), // See CustomMaximumNArgs() below + Use: "in-toto [IMAGE-NAME | FILE-PATH | DIR-PATH]", + Short: attestInTotoShortDesc, + Long: attestInTotoLongDesc, + Example: attestInTotoExample, + PreRunE: func(cmd *cobra.Command, args []string) error { + err := CustomMaximumNArgs(1, args) + if err != nil { + return err + } + + err = RequireGlobalFlags(global, []string{"Org", "ApiToken"}) + if err != nil { + return ErrorBeforePrintingUsage(cmd, err.Error()) + } + + err = MuXRequiredFlags(cmd, []string{"fingerprint", "artifact-type"}, true) + if err != nil { + return err + } + + err = ValidateAttestationArtifactArg(args, o.fingerprintOptions.artifactType, o.fingerprint) + if err != nil { + return ErrorBeforePrintingUsage(cmd, err.Error()) + } + + // Cobra checks required flags only after PreRunE, and validate + // would otherwise report an empty --predicate-type as an unknown + // one rather than a missing one. + if err := cmd.ValidateRequiredFlags(); err != nil { + return err + } + if err := o.validate(); err != nil { + return err + } + + return ValidateRegistryFlags(cmd, o.fingerprintOptions) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return o.run(args) + }, + } + + cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", attestationFingerprintFlag) + cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag) + cmd.Flags().StringVarP(&o.trailName, "trail", "T", "", trailNameFlag) + cmd.Flags().StringVarP(&o.name, "name", "n", "", intotoNameFlag) + cmd.Flags().StringVar(&o.source, "source", intotoSourceBundle, intotoSourceFlag) + cmd.Flags().StringVar(&o.bundlePath, "bundle", "", intotoBundleFlag) + cmd.Flags().StringVar(&o.predicateType, "predicate-type", "", intotoPredicateTypeFlag) + addSignerPolicyFlags(cmd, &o.policy) + cmd.Flags().BoolVar(&o.policy.AllowUnsigned, "allow-unsigned", false, allowUnsignedFlag) + cmd.Flags().StringVar(&o.buildURL, "build-url", "", intotoBuildURLFlag) + cmd.Flags().StringVar(&o.commitURL, "commit-url", "", intotoCommitURLFlag) + cmd.Flags().StringVar(&o.repoProvider, "repo-provider", "", intotoRepoProviderFlag) + cmd.Flags().StringVar(&o.repoRoot, "repo-root", ".", intotoRepoRootFlag) + cmd.Flags().StringVar(&o.githubToken, "github-token", "", intotoGithubTokenFlag) + cmd.Flags().StringVar(&o.githubBaseURL, "github-base-url", "", intotoGithubBaseURLFlag) + cmd.Flags().StringToStringVar(&o.annotations, "annotate", map[string]string{}, annotationFlag) + cmd.Flags().StringToStringVar(&o.externalURLs, "external-url", map[string]string{}, externalURLFlag) + cmd.Flags().StringToStringVar(&o.externalFingerprints, "external-fingerprint", map[string]string{}, externalFingerprintFlag) + addFingerprintFlags(cmd, o.fingerprintOptions) + addDryRunFlag(cmd) + + err := RequireFlags(cmd, []string{"flow", "trail", "name", "predicate-type"}) + if err != nil { + logger.Error("failed to configure required flags: %v", err) + } + + return cmd +} + +// addSignerPolicyFlags adds the flags that pin who must have signed an +// attestation. They mirror cosign verify-attestation and gh attestation +// verify, so a policy written for either carries across. +func addSignerPolicyFlags(cmd *cobra.Command, policy *intoto.Policy) { + cmd.Flags().StringVar(&policy.CertificateIdentity, "certificate-identity", "", certificateIdentityFlag) + cmd.Flags().StringVar(&policy.CertificateIdentityRegexp, "certificate-identity-regexp", "", certificateIdentityRegexpFlag) + cmd.Flags().StringVar(&policy.CertificateOIDCIssuer, "certificate-oidc-issuer", "", certificateOIDCIssuerFlag) + cmd.Flags().StringVar(&policy.KeyPath, "key", "", signerKeyFlag) +} + +// validate checks everything that needs nothing from the attestation, so a +// mistake costs neither a fingerprint calculation nor a file read. +func (o *attestInTotoOptions) validate() error { + if o.source != intotoSourceBundle { + return fmt.Errorf("--source %q is not supported. Must be: %s", o.source, intotoSourceBundle) + } + if o.bundlePath == "" { + return fmt.Errorf("--bundle is required with --source %s", intotoSourceBundle) + } + + kind, err := intoto.ParsePredicateKind(o.predicateType) + if err != nil { + return fmt.Errorf("%s for --predicate-type", err) + } + o.kind = kind + + if err := o.policy.Validate(); err != nil { + return err + } + + if kind == intoto.KindProvenance && !templateArtifactNamePattern.MatchString(o.name) { + return fmt.Errorf("--name '%s' is not a valid artifact name. With --predicate-type provenance it names the artifact in the flow template, which can only contain letters, digits, '-' and '_'", o.name) + } + if kind == intoto.KindSBOM { + if _, _, err := parseAttestationNameTemplate(o.name); err != nil { + return fmt.Errorf("--name '%s' is not a valid attestation name: %s", o.name, err) + } + } + if err := validateRepoFlags("", o.repoProvider, false); err != nil { + return err + } + + for _, reserved := range o.reservedAnnotations() { + if _, taken := o.annotations[reserved]; taken { + return fmt.Errorf("annotation key '%s' is set by this command from the attestation and cannot be provided with --annotate", reserved) + } + } + if _, taken := o.externalURLs[attestationExternalURL]; taken { + return fmt.Errorf("external URL label '%s' is set by this command from the attestation and cannot be provided with --external-url", attestationExternalURL) + } + return nil +} + +func (o *attestInTotoOptions) reservedAnnotations() []string { + reserved := []string{verifiedAnnotation, signerIdentityAnnotation, signerIssuerAnnotation, + logIndexAnnotation, integratedTimeAnnotation, signedTimestampAnnotation} + if o.kind == intoto.KindSBOM { + return append(reserved, sbomFormatAnnotation, sbomSha256Annotation) + } + return append(reserved, builderIDAnnotation, buildTypeAnnotation, workflowAnnotation) +} + +func (o *attestInTotoOptions) run(args []string) error { + if o.fingerprint == "" { + var err error + o.fingerprint, err = GetSha256Digest(args[0], o.fingerprintOptions, logger) + if err != nil { + return fmt.Errorf("failed to calculate artifact fingerprint: %s", err) + } + } + + found, err := intoto.Find(intoto.BundleFile{Path: o.bundlePath}, o.kind, o.fingerprint, + intoto.NewVerifier(o.policy, intotoTrustRoots())) + if err != nil { + return err + } + for _, rejected := range found.Rejected { + logger.Warn("another %s attestation was not recorded: %s", o.kind, rejected) + } + + externalURLs, err := processExternalURLs(o.externalURLs, o.externalFingerprints) + if err != nil { + return err + } + if found.Evidence.LogEntryURL != "" { + externalURLs[attestationExternalURL] = &URLInfo{Href: found.Evidence.LogEntryURL, Fingerprint: found.Evidence.RawSHA256} + } + annotations, err := processAnnotations(o.annotations) + if err != nil { + return err + } + annotations = withAnnotations(annotations, verificationAnnotations(found.Evidence)) + + if o.kind == intoto.KindSBOM { + return o.recordSBOM(found, externalURLs, annotations) + } + return o.recordProvenance(args, found, externalURLs, annotations) +} + +func (o *attestInTotoOptions) recordProvenance(args []string, found *intoto.Found, externalURLs map[string]*URLInfo, annotations map[string]string) error { + provenance, err := intoto.ProvenanceFrom(found.Attestation, o.fingerprint, o.repoProvider) + if err != nil { + return err + } + buildURL, commitURL := provenance.BuildURL, provenance.CommitURL + var missingFields, missingFlags []string + noRepository := false + for _, field := range provenance.Missing { + switch field { + case intoto.MissingCommit: + return fmt.Errorf("the provenance names no git commit, and Kosli records an artifact by the commit it was built from") + case intoto.MissingBuildURL: + buildURL = o.buildURL + if buildURL == "" { + missingFields, missingFlags = append(missingFields, field), append(missingFlags, "--build-url") + } + case intoto.MissingCommitURL: + commitURL = o.commitURL + if commitURL == "" { + missingFields, missingFlags = append(missingFields, field), append(missingFlags, "--commit-url") + } + case intoto.MissingRepository: + noRepository = true + } + } + if len(missingFlags) > 0 { + return fmt.Errorf("the provenance does not say the %s, which Kosli records an artifact with. Pass %s", + strings.Join(missingFields, " or the "), strings.Join(missingFlags, " and ")) + } + if noRepository { + logger.Warn("the provenance does not name the repository it was built from, so no repository is recorded") + } + + payload := AttestArtifactPayload{ + Fingerprint: o.fingerprint, + Filename: firstNonEmpty(provenance.Filename, argOrEmpty(args), o.fingerprint), + GitCommit: provenance.Commit, + GitCommitInfo: o.commitInfo(provenance), + BuildUrl: buildURL, + CommitUrl: commitURL, + RepoUrl: provenance.Repository.URL, + Name: o.name, + TrailName: o.trailName, + ExternalURLs: externalURLs, + Annotations: withAnnotations(annotations, nonEmpty(map[string]string{ + builderIDAnnotation: provenance.BuilderID, + buildTypeAnnotation: provenance.BuildType, + workflowAnnotation: provenance.Workflow, + })), + } + if provenance.Repository.URL != "" { + payload.GitRepoInfo = mergeGitRepoInfo(nil, provenance.Repository.ID, provenance.Repository.Name, + provenance.Repository.URL, provenance.Repository.Provider, true) + } + + endpoint, err := url.JoinPath(global.Host, "api/v2/artifacts", global.Org, o.flowName) + if err != nil { + return err + } + _, err = kosliClient.Do(&requests.RequestParams{ + Method: http.MethodPost, + URL: endpoint, + Payload: payload, + DryRun: global.DryRun, + Token: global.ApiToken, + }) + if err == nil && !global.DryRun { + logger.Info("artifact %s was attested with fingerprint: %s", payload.Filename, payload.Fingerprint) + } + return err +} + +// commitInfo returns the details of the commit the provenance names. They are +// looked up in the repository the statement names, not in the one this +// command runs in, then read from a local checkout. Failing both it returns +// nil, leaving the commit recorded by its SHA alone: POST /artifacts refuses +// commit info that lacks the message, author, timestamp or branch, although +// the API's schema marks them optional. +func (o *attestInTotoOptions) commitInfo(provenance *intoto.Provenance) *gitview.BasicCommitInfo { + if finder := o.commitFinder(provenance.Repository); finder != nil { + commit, err := finder.CommitBySHA(provenance.Repository.Name, provenance.Commit) + if err == nil { + return &gitview.BasicCommitInfo{ + Sha1: commit.SHA, + Message: commit.Message, + Author: commit.Author, + Timestamp: commit.Timestamp, + Branch: provenance.Branch, + URL: commit.URL, + } + } + logger.Warn("could not look up commit %s: %s", provenance.Commit, err) + } + + if gitView, err := gitview.New(o.repoRoot); err == nil { + if local, err := gitView.GetCommitInfoFromCommitSHA(provenance.Commit, false, nil); err == nil { + info := local.BasicCommitInfo + // The checkout's branch is this job's, not necessarily the build's. + info.Branch = provenance.Branch + return &info + } + } + + logger.Warn("the details and branch of commit %s are not recorded, only its SHA: they could not be looked up, and the git repository at --repo-root %s does not hold the commit", provenance.Commit, o.repoRoot) + return nil +} + +// commitFinder chooses how to look up a commit by the repository's provider. +// GitHub is the only provider implemented; a lookup also needs a token. +func (o *attestInTotoOptions) commitFinder(repo intoto.Repository) types.CommitFinder { + if repo.Provider == "github" && o.githubToken != "" { + return ghUtils.CommitFinder{Token: o.githubToken, BaseURL: o.githubBaseURL, Debug: global.Debug} + } + return nil +} + +func (o *attestInTotoOptions) recordSBOM(found *intoto.Found, externalURLs map[string]*URLInfo, annotations map[string]string) error { + document, err := intoto.SBOMDocument(found.Attestation) + if err != nil { + return err + } + data, err := sbomAttestationData(found.Raw, document) + if err != nil { + return fmt.Errorf("failed to parse the SBOM in %s: %s", intoto.BundleFile{Path: o.bundlePath}, err) + } + + common := &CommonAttestationPayload{ + ArtifactFingerprint: o.fingerprint, + ExternalURLs: externalURLs, + Annotations: withAnnotations(annotations, sbomAnnotations(data)), + } + artifactName, attestationName, err := parseAttestationNameTemplate(o.name) + if err != nil { + return err + } + if attestationName != "" { + common.TargetArtifacts = []string{artifactName} + common.AttestationName = attestationName + } else { + common.AttestationName = artifactName + } + + return newAttestationSubmitter().submit(attestationSubmission{ + flow: o.flowName, + trail: o.trailName, + // The slug is the attestation family, not the type. The server tells + // them apart by type_name in the body. + slug: "system", + label: "sbom", + payload: SbomAttestationPayload{ + CommonAttestationPayload: common, + TypeName: "sbom", + AttestationData: data, + }, + evidence: &requests.FileBytes{Name: o.attachmentName(found.Attestation), Data: found.Raw}, + }) +} + +// attachmentName names the attached bytes after the file they came from, and +// after their line when the file holds several attestations. +func (o *attestInTotoOptions) attachmentName(a *intoto.Attestation) string { + name := filepath.Base(o.bundlePath) + if a.Position == 0 { + return name + } + return fmt.Sprintf("%s.%d.json", strings.TrimSuffix(name, filepath.Ext(name)), a.Position) +} + +// verificationAnnotations records what verifying the attestation established +// where a reader sees it on the trail page. +func verificationAnnotations(evidence *intoto.Evidence) map[string]string { + annotations := map[string]string{ + verifiedAnnotation: strconv.FormatBool(evidence.Verified), + signerIdentityAnnotation: evidence.SignerIdentity, + signerIssuerAnnotation: evidence.SignerIssuer, + } + if evidence.LogIndex != nil { + annotations[logIndexAnnotation] = strconv.FormatInt(*evidence.LogIndex, 10) + } + if !evidence.IntegratedTime.IsZero() { + annotations[integratedTimeAnnotation] = evidence.IntegratedTime.Format(time.RFC3339) + } + if !evidence.SignedTime.IsZero() { + annotations[signedTimestampAnnotation] = evidence.SignedTime.Format(time.RFC3339) + } + return nonEmpty(annotations) +} + +func nonEmpty(m map[string]string) map[string]string { + kept := make(map[string]string, len(m)) + for key, value := range m { + if value != "" { + kept[key] = value + } + } + return kept +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +func argOrEmpty(args []string) string { + if len(args) == 0 { + return "" + } + return args[0] +} diff --git a/cmd/kosli/attestInToto_test.go b/cmd/kosli/attestInToto_test.go new file mode 100644 index 000000000..f15aa1ac6 --- /dev/null +++ b/cmd/kosli/attestInToto_test.go @@ -0,0 +1,443 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/kosli-dev/cli/internal/gitview" + "github.com/kosli-dev/cli/internal/intoto" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +const ( + intotoFixtures = "../../internal/intoto/testdata" + intotoArtifact = intotoFixtures + "/artifact.bin" + intotoDigest = "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8" + intotoCommit = "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70" + intotoSigner = "--certificate-identity https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main --certificate-oidc-issuer https://token.actions.githubusercontent.com" + intotoBuildURL = "https://github.com/acme/widget/actions/runs/1234567890/attempts/1" + intotoCommitURL = "https://github.com/acme/widget/commit/" + intotoCommit + intotoLogEntryURL = "https://rekor.localhost/api/v1/log/entries?logIndex=0" + intotoSBOMTemplate = "version: 1\ntrail:\n artifacts:\n - name: app\n attestations:\n - name: sbom\n type: sbom\n" + intotoDefaultCommand = "attest in-toto " + intotoArtifact + " --artifact-type file" +) + +type AttestInTotoCommandTestSuite struct { + suite.Suite + flowName string + trailName string + defaultKosliArguments string + github *httptest.Server + githubArguments string + savedTrustRoots func() *intoto.TrustRoots +} + +func (suite *AttestInTotoCommandTestSuite) SetupSuite() { + // GitHub's commits API, serving the one commit the provenance fixtures name. + suite.github = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v3/repos/acme/widget/commits/"+intotoCommit { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + return + } + _, _ = fmt.Fprintf(w, `{ + "sha": %q, + "html_url": %q, + "author": {"login": "jdoe"}, + "commit": { + "message": "Add the widget", + "author": {"name": "Jane Doe", "email": "jane@example.com", "date": "2026-11-01T09:00:00Z"} + } + }`, intotoCommit, intotoCommitURL) + })) + suite.githubArguments = fmt.Sprintf(" --github-token a-token --github-base-url %s/", suite.github.URL) +} + +func (suite *AttestInTotoCommandTestSuite) TearDownSuite() { + suite.github.Close() +} + +func (suite *AttestInTotoCommandTestSuite) SetupTest() { + suite.flowName = "attest-in-toto" + suite.trailName = "test-123" + global = &GlobalOpts{ + ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY", + Org: "docs-cmd-test-user", + Host: "http://localhost:8001", + } + suite.defaultKosliArguments = fmt.Sprintf(" --flow %s --trail %s --host %s --org %s --api-token %s", + suite.flowName, suite.trailName, global.Host, global.Org, global.ApiToken) + + trustedRoot, err := root.NewTrustedRootFromPath(filepath.Join(intotoFixtures, "trusted_root.json")) + require.NoError(suite.T(), err) + suite.savedTrustRoots = intotoTrustRoots + intotoTrustRoots = func() *intoto.TrustRoots { return intoto.FixedTrustRoots(trustedRoot) } + + template := filepath.Join(suite.T().TempDir(), "template.yml") + require.NoError(suite.T(), os.WriteFile(template, []byte(intotoSBOMTemplate), 0o600)) + CreateFlowWithTemplate(suite.flowName, template, suite.T()) + BeginTrail(suite.trailName, suite.flowName, "", suite.T()) +} + +func (suite *AttestInTotoCommandTestSuite) TearDownTest() { + intotoTrustRoots = suite.savedTrustRoots +} + +func (suite *AttestInTotoCommandTestSuite) command(flags string) string { + return intotoDefaultCommand + " " + flags + suite.defaultKosliArguments +} + +func (suite *AttestInTotoCommandTestSuite) bundle(name string) string { + return " --bundle " + filepath.Join(intotoFixtures, name) +} + +func fixtureSHA256(t *testing.T, name string) string { + t.Helper() + content, err := os.ReadFile(filepath.Join(intotoFixtures, name)) + require.NoError(t, err) + return fmt.Sprintf("%x", sha256.Sum256(content)) +} + +// statementVariant writes a provenance statement fixture with each old string +// replaced by its new one, and returns the file's path. +func statementVariant(t *testing.T, replacements ...string) string { + t.Helper() + content, err := os.ReadFile(filepath.Join(intotoFixtures, "statements", "github-provenance.json")) + require.NoError(t, err) + statement := string(content) + for i := 0; i < len(replacements); i += 2 { + require.Contains(t, statement, replacements[i]) + statement = strings.ReplaceAll(statement, replacements[i], replacements[i+1]) + } + path := filepath.Join(t.TempDir(), "provenance.json") + require.NoError(t, os.WriteFile(path, []byte(statement), 0o600)) + return path +} + +func (suite *AttestInTotoCommandTestSuite) TestProvenanceBuildsTheArtifactFromTheStatement() { + bundleSHA256 := fixtureSHA256(suite.T(), "github-provenance.sigstore.json") + signedCommand := suite.command("--predicate-type provenance --name app --dry-run " + intotoSigner + + suite.bundle("github-provenance.sigstore.json") + suite.githubArguments) + + runTestCmd(suite.T(), []cmdTestCase{ + { + name: "posts the artifact with the subject's fingerprint and filename", + cmd: signedCommand, + goldenRegex: `(?s)artifacts/docs-cmd-test-user/attest-in-toto.*"fingerprint": "` + intotoDigest + `",\s+"filename": "artifact.bin",\s+"git_commit": "` + intotoCommit + `"`, + }, + { + name: "records the build's run and commit URL from the statement", + cmd: signedCommand, + goldenRegex: `(?s)"build_url": "` + intotoBuildURL + `",\s+"commit_url": "` + intotoCommitURL + `",\s+"repo_url": "https://github.com/acme/widget",\s+"template_reference_name": "app"`, + }, + { + name: "records the repository the statement names", + cmd: signedCommand, + goldenRegex: `(?s)"repo_info": \{\s+"url": "https://github.com/acme/widget",\s+"name": "acme/widget",\s+"id": "123456789",\s+"provider": "github"\s+\}`, + }, + { + name: "looks the commit's details up on GitHub, keeping the statement's branch", + cmd: signedCommand, + goldenRegex: `(?s)"git_commit_info": \{\s+"sha1": "` + intotoCommit + `",\s+"message": "Add the widget",\s+"author": "Jane Doe \\u003cjane@example.com\\u003e",\s+"timestamp": 1793523600,\s+"branch": "main",\s+"url": "` + intotoCommitURL + `"\s+\}`, + }, + { + name: "links the transparency log entry, pinned by the bundle's SHA-256", + cmd: signedCommand, + goldenRegex: `(?s)"external_urls": \{\s+"attestation": \{\s+"href": "` + regexpQuote(intotoLogEntryURL) + `",\s+"fingerprint": "` + bundleSHA256 + `"`, + }, + { + name: "annotates the builder, the build type, the workflow and the verification", + cmd: signedCommand, + goldenRegex: `(?s)"build_type": "https://actions.github.io/buildtypes/workflow/v1",\s+"builder_id": "https://github.com/actions/runner/github-hosted",\s+"integrated_time": "[^"]+",\s+"log_index": "0",\s+"signed_timestamp": "[^"]+",\s+"signer_identity": "https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main",\s+"signer_issuer": "https://token.actions.githubusercontent.com",\s+"verified": "true",\s+"workflow": ".github/workflows/build.yml"`, + }, + { + name: "keeps the caller's own annotations and external URLs", + cmd: signedCommand + " --annotate team=platform --external-url docs=https://example.com/docs", + goldenRegex: `(?s)"docs": \{\s+"href": "https://example.com/docs"\s+\}.*"team": "platform"`, + }, + { + name: "reads BuildKit provenance", + cmd: suite.command("--predicate-type provenance --name app --dry-run " + intotoSigner + suite.bundle("buildkit-provenance.sigstore.json") + suite.githubArguments), + goldenRegex: `(?s)"filename": "pkg:docker/acme/widget@1.2.3\?platform=linux%2Famd64".*"build_url": "` + intotoBuildURL + `".*"build_type": "https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md"`, + }, + { + name: "reads SLSA v0.2 provenance", + cmd: suite.command("--predicate-type provenance --name app --dry-run " + intotoSigner + suite.bundle("slsa-v0.2-provenance.sigstore.json") + suite.githubArguments), + goldenRegex: `(?s)"build_url": "` + intotoBuildURL + `".*"workflow": ".github/workflows/release.yml"`, + }, + { + name: "finds the provenance among other attestations in a .intoto.jsonl file", + cmd: suite.command("--predicate-type provenance --name app --dry-run " + intotoSigner + suite.bundle("attestations.intoto.jsonl") + suite.githubArguments), + goldenRegex: `(?s)"git_commit": "` + intotoCommit + `".*"verified": "true"`, + }, + { + name: "verifies a DSSE envelope with the key that signed it", + cmd: suite.command("--predicate-type provenance --name app --dry-run --key " + filepath.Join(intotoFixtures, "key.pub") + suite.bundle("key-signed-provenance.dsse.json") + suite.githubArguments), + goldenRegex: `(?s)"annotations": \{\s+"build_type": "[^"]+",\s+"builder_id": "[^"]+",\s+"verified": "true",`, + }, + { + name: "records an unsigned statement as unverified when allowed", + cmd: suite.command("--predicate-type provenance --name app --dry-run --allow-unsigned --bundle " + filepath.Join(intotoFixtures, "statements", "github-provenance.json") + suite.githubArguments), + goldenRegex: `(?s)"verified": "false"`, + }, + }) +} + +func (suite *AttestInTotoCommandTestSuite) TestProvenanceWhenTheStatementLacksDetails() { + // CI sets a real token, which would send the cases that expect no lookup to api.github.com. + suite.T().Setenv("KOSLI_GITHUB_TOKEN", "") + noURLs := statementVariant(suite.T(), + `"invocationId": "`+intotoBuildURL+`"`, `"invocationId": "local-17"`, + "https://github.com/acme/widget", "https://git.example.com/acme/widget") + + runTestCmd(suite.T(), []cmdTestCase{ + { + wantError: true, + name: "fails naming the build URL and commit URL it needs, and the flags that supply them", + cmd: suite.command("--predicate-type provenance --name app --allow-unsigned --bundle " + noURLs), + golden: "Error: the provenance does not say the build URL or the commit URL, which Kosli records an artifact with. Pass --build-url and --commit-url\n", + }, + { + name: "takes the build URL and commit URL from the flags when the statement lacks them", + cmd: suite.command("--predicate-type provenance --name app --dry-run --allow-unsigned --bundle " + noURLs + " --build-url https://ci.example.com/17 --commit-url https://git.example.com/acme/widget/commit/" + intotoCommit), + goldenRegex: `(?s)"build_url": "https://ci.example.com/17",\s+"commit_url": "https://git.example.com/acme/widget/commit/` + intotoCommit + `"`, + }, + { + name: "prefers the statement's build URL to the flag's", + cmd: suite.command("--predicate-type provenance --name app --dry-run " + intotoSigner + suite.bundle("github-provenance.sigstore.json") + " --build-url https://ci.example.com/this-job" + suite.githubArguments), + goldenRegex: `"build_url": "` + intotoBuildURL + `"`, + }, + { + // POST /artifacts refuses commit info without every field, so none is sent. + name: "records only the commit's SHA when its details cannot be found", + cmd: suite.command("--predicate-type provenance --name app --dry-run " + intotoSigner + suite.bundle("github-provenance.sigstore.json")), + goldenRegex: `(?s)^\[warning\] the details and branch of commit ` + intotoCommit + ` are not recorded, only its SHA.*"git_commit": "` + intotoCommit + `",\s+"git_commit_info": null`, + }, + { + name: "has the server accept an artifact known only by its commit's SHA", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("github-provenance.sigstore.json")), + golden: "[warning] the details and branch of commit " + intotoCommit + " are not recorded, only its SHA: they could not be looked up, and the git repository at --repo-root . does not hold the commit\nartifact artifact.bin was attested with fingerprint: " + intotoDigest + "\n", + }, + { + name: "warns when GitHub cannot find the commit, then records only its SHA", + cmd: suite.command("--predicate-type provenance --name app --dry-run --allow-unsigned --bundle " + statementVariant(suite.T(), "acme/widget", "acme/gadget") + suite.githubArguments), + goldenRegex: `(?s)^\[warning\] could not look up commit ` + intotoCommit + `: failed to get commit ` + intotoCommit + ` from GitHub repository acme/gadget.*"git_commit_info": null`, + }, + { + name: "reads the commit's details from a local checkout that holds it", + cmd: suite.command("--predicate-type provenance --name app --dry-run --allow-unsigned --repo-root ../.. --bundle " + statementVariant(suite.T(), intotoCommit, localHeadCommit(suite.T()))), + goldenRegex: `(?s)"git_commit_info": \{\s+"sha1": "` + localHeadCommit(suite.T()) + `",\s+"message": ` + jsonString + `,\s+"author": ` + jsonString + `,\s+"timestamp": \d+,\s+"branch": "main"`, + }, + }) +} + +// jsonString matches any JSON-encoded string, including escaped quotes, because +// the local checkout's commit message and author are not under the test's control. +const jsonString = `"(?:[^"\\]|\\.)+"` + +func (suite *AttestInTotoCommandTestSuite) TestSBOMIsRecordedLikeAttestSbom() { + bundleSHA256 := fixtureSHA256(suite.T(), "spdx-sbom.sigstore.json") + signedCommand := suite.command("--predicate-type sbom --name app.sbom --dry-run " + intotoSigner + suite.bundle("spdx-sbom.sigstore.json")) + + runTestCmd(suite.T(), []cmdTestCase{ + { + name: "posts an sbom attestation to the system endpoint, named for the artifact in the template", + cmd: signedCommand, + goldenRegex: `(?s)trail/test-123/system.*"artifact_fingerprint": "` + intotoDigest + `",\s+"attestation_name": "sbom",\s+"target_artifacts": \[\s+"app"\s+\].*"type_name": "sbom"`, + }, + { + name: "summarises the SBOM and fingerprints the attached bundle", + cmd: signedCommand, + goldenRegex: `(?s)"attestation_data": \{\s+"format": "spdx-2\.3",\s+"original_fingerprint": "` + bundleSHA256 + `",\s+"document": \{.*"package_count": 1`, + }, + { + name: "annotates the SBOM and its verification", + cmd: signedCommand, + goldenRegex: `(?s)"sbom_format": "spdx-2\.3",\s+"sbom_sha256": "` + bundleSHA256 + `",.*"verified": "true"`, + }, + { + name: "uploads the bundle as the attachment", + cmd: signedCommand, + goldenRegex: `(?s)"filename": "spdx-sbom.sigstore.json".*"sha256": "` + bundleSHA256 + `"`, + }, + { + name: "reads CycloneDX", + cmd: suite.command("--predicate-type sbom --name container-sbom --dry-run " + intotoSigner + suite.bundle("cyclonedx-sbom.sigstore.json")), + goldenRegex: `(?s)"attestation_name": "container-sbom".*"format": "cyclonedx-1\.6"`, + }, + { + name: "attaches only the matching line of a .intoto.jsonl file", + cmd: suite.command("--predicate-type sbom --name app.sbom --dry-run " + intotoSigner + suite.bundle("attestations.intoto.jsonl")), + goldenRegex: `"filename": "attestations.intoto.3.json"`, + }, + }) +} + +func (suite *AttestInTotoCommandTestSuite) TestRecordsSatisfyTheTemplate() { + runTestCmd(suite.T(), []cmdTestCase{ + { + name: "records the artifact named app", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("github-provenance.sigstore.json") + suite.githubArguments), + golden: fmt.Sprintf("artifact artifact.bin was attested with fingerprint: %s\n", intotoDigest), + }, + { + name: "records the SBOM named app.sbom", + cmd: suite.command("--predicate-type sbom --name app.sbom " + intotoSigner + suite.bundle("spdx-sbom.sigstore.json")), + golden: "sbom attestation 'sbom' is reported to trail: test-123\n", + }, + { + name: "leaves the trail with both of the template's records", + cmd: fmt.Sprintf("get trail %s --flow %s --output json --host %s --org %s --api-token %s", suite.trailName, suite.flowName, global.Host, global.Org, global.ApiToken), + goldenJson: []jsonCheck{ + {"compliance_status.status", "COMPLIANT"}, + {"compliance_status.artifacts_statuses.app.artifact_fingerprint", intotoDigest}, + {"compliance_status.artifacts_statuses.app.attestations_statuses.[0].attestation_name", "sbom"}, + {"compliance_status.artifacts_statuses.app.attestations_statuses.[0].unexpected", false}, + }, + }, + }) +} + +func (suite *AttestInTotoCommandTestSuite) TestNameFromTheEnvironment() { + suite.T().Setenv("KOSLI_NAME", "app") + runTestCmd(suite.T(), []cmdTestCase{ + { + name: "takes --name from KOSLI_NAME, as attest artifact and attest sbom do", + cmd: suite.command("--predicate-type provenance --dry-run " + intotoSigner + suite.bundle("github-provenance.sigstore.json")), + goldenRegex: `"template_reference_name": "app"`, + }, + }) +} + +func (suite *AttestInTotoCommandTestSuite) TestRefusesWhatItCannotTrust() { + runTestCmd(suite.T(), []cmdTestCase{ + { + wantError: true, + name: "a statement about another artifact", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("wrong-subject-provenance.sigstore.json")), + golden: "Error: bundle file " + intotoFixtures + "/wrong-subject-provenance.sigstore.json: no provenance attestation lists the artifact (sha256:" + intotoDigest + ") as its subject; the subjects found were: other.bin (sha256:11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8)\n", + }, + { + wantError: true, + name: "a bundle signed by another identity", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("wrong-signer-provenance.sigstore.json")), + goldenRegex: `^Error: bundle file .*/wrong-signer-provenance.sigstore.json: the attestation failed verification: .*https://github.com/mallory/widget`, + }, + { + wantError: true, + name: "a file where one attestation has the wrong signer, though another is unsigned and allowed", + cmd: suite.command("--predicate-type provenance --name app --allow-unsigned " + intotoSigner + suite.bundle("wrong-signer-and-unsigned.intoto.jsonl")), + goldenRegex: `^Error: bundle file .*/wrong-signer-and-unsigned.intoto.jsonl: attestation 1 failed verification: .*https://github.com/mallory/widget`, + }, + { + wantError: true, + name: "a key-signed envelope checked against an identity, which cannot be verified rather than failing", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("key-signed-provenance.dsse.json")), + golden: "Error: bundle file " + intotoFixtures + "/key-signed-provenance.dsse.json: the attestation could not be verified: a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope with --key\n", + }, + { + wantError: true, + name: "a statement changed after signing", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("tampered-provenance.sigstore.json")), + goldenRegex: `^Error: bundle file .*/tampered-provenance.sigstore.json: the attestation failed verification: `, + }, + { + wantError: true, + name: "a signed bundle with no signer pinned, naming the flags that pin one", + cmd: suite.command("--predicate-type provenance --name app" + suite.bundle("github-provenance.sigstore.json")), + golden: "Error: bundle file " + intotoFixtures + "/github-provenance.sigstore.json: the attestation is signed, so its signer must be pinned to verify it. Pass --certificate-identity or --certificate-identity-regexp with --certificate-oidc-issuer, or --key\n", + }, + { + wantError: true, + name: "an unsigned statement without --allow-unsigned", + cmd: suite.command("--predicate-type provenance --name app --bundle " + filepath.Join(intotoFixtures, "statements", "buildkit-provenance.json")), + golden: "Error: bundle file " + intotoFixtures + "/statements/buildkit-provenance.json: the attestation is not signed. Pass --allow-unsigned to record it as unverified\n", + }, + { + wantError: true, + name: "a file holding only other kinds of attestation, naming them", + cmd: suite.command("--predicate-type provenance --name app " + intotoSigner + suite.bundle("vuln-only.intoto.jsonl")), + golden: "Error: bundle file " + intotoFixtures + "/vuln-only.intoto.jsonl: found no provenance attestation (SLSA provenance v1 or v0.2); the predicate types found were: https://cosign.sigstore.dev/attestation/vuln/v1\n", + }, + }) +} + +func (suite *AttestInTotoCommandTestSuite) TestRejectsBadFlags() { + runTestCmd(suite.T(), []cmdTestCase{ + { + wantError: true, + name: "missing --predicate-type and --name, listing both", + cmd: suite.command(suite.bundle("github-provenance.sigstore.json")), + golden: "Error: required flag(s) \"name\", \"predicate-type\" not set\n", + }, + { + wantError: true, + name: "a predicate type it does not record", + cmd: suite.command("--predicate-type vuln --name app" + suite.bundle("github-provenance.sigstore.json")), + golden: "Error: predicate type \"vuln\" is not supported. Must be one of: provenance, sbom for --predicate-type\n", + }, + { + wantError: true, + name: "a source other than bundle", + cmd: suite.command("--source github --predicate-type provenance --name app"), + golden: "Error: --source \"github\" is not supported. Must be: bundle\n", + }, + { + wantError: true, + name: "no --bundle", + cmd: suite.command("--predicate-type provenance --name app"), + golden: "Error: --bundle is required with --source bundle\n", + }, + { + wantError: true, + name: "an artifact name with a dot for provenance", + cmd: suite.command("--predicate-type provenance --name app.sbom" + suite.bundle("github-provenance.sigstore.json")), + golden: "Error: --name 'app.sbom' is not a valid artifact name. With --predicate-type provenance it names the artifact in the flow template, which can only contain letters, digits, '-' and '_'\n", + }, + { + wantError: true, + name: "an identity without an issuer", + cmd: suite.command("--predicate-type provenance --name app --certificate-identity someone" + suite.bundle("github-provenance.sigstore.json")), + golden: "Error: --certificate-oidc-issuer is required with --certificate-identity or --certificate-identity-regexp\n", + }, + { + wantError: true, + name: "an annotation the command sets itself", + cmd: suite.command("--predicate-type provenance --name app --annotate verified=true" + suite.bundle("github-provenance.sigstore.json")), + golden: "Error: annotation key 'verified' is set by this command from the attestation and cannot be provided with --annotate\n", + }, + { + wantError: true, + name: "neither --fingerprint nor --artifact-type", + cmd: "attest in-toto --predicate-type provenance --name app" + suite.bundle("github-provenance.sigstore.json") + suite.defaultKosliArguments, + golden: "Error: at least one of --fingerprint, --artifact-type is required\n", + }, + }) +} + +// localHeadCommit is a commit the repository these tests run in is sure to +// hold, standing in for the one a build's provenance names. +func localHeadCommit(t *testing.T) string { + t.Helper() + gitView, err := gitview.New("../..") + require.NoError(t, err) + head, err := gitView.GetCommitInfoFromCommitSHA("HEAD", false, nil) + require.NoError(t, err) + return head.Sha1 +} + +func regexpQuote(s string) string { + return strings.NewReplacer(".", `\.`, "?", `\?`).Replace(s) +} + +func TestAttestInTotoCommandTestSuite(t *testing.T) { + suite.Run(t, new(AttestInTotoCommandTestSuite)) +} diff --git a/cmd/kosli/attestSbom.go b/cmd/kosli/attestSbom.go index 8de1966e7..e85e4bbf7 100644 --- a/cmd/kosli/attestSbom.go +++ b/cmd/kosli/attestSbom.go @@ -2,11 +2,13 @@ package main import ( "crypto/sha256" + "errors" "fmt" "io" "os" "path/filepath" + "github.com/kosli-dev/cli/internal/intoto" "github.com/kosli-dev/cli/internal/requests" "github.com/kosli-dev/cli/internal/sbom" "github.com/spf13/cobra" @@ -34,6 +36,7 @@ type SbomAttestationPayload struct { type attestSbomOptions struct { *CommonAttestationOptions sbomFilePath string + policy intoto.Policy payload SbomAttestationPayload } @@ -49,8 +52,20 @@ not compressed, and a gzipped file is rejected, because the format and the summary below are read from it. Kosli reads the format, the creation time, the tools that produced it, the -subject it describes and how many packages it lists. Nothing is checked against -the artifact; the SBOM is recorded as reported. +subject it describes and how many packages it lists. A plain SBOM file is not +checked against the artifact; it is recorded as reported. + +The SBOM may also arrive inside an in-toto attestation, as a Sigstore bundle or +a DSSE envelope. The SPDX or CycloneDX document is then read from it, and the +bundle or envelope is what is uploaded and checksummed. When the SBOM is about +an artifact, the attestation must list that artifact as its subject, signed or +not. To verify its signature, pass ^--certificate-identity^ (or +^--certificate-identity-regexp^) with ^--certificate-oidc-issuer^, or ^--key^. +Without them the signature is not checked, and even a signed bundle is recorded +with ^verified^ set to false. The record is annotated with ^verified^ and the +verification details, as ^kosli attest in-toto^ annotates it. To pick the SBOM +from a file holding several attestations, or to record provenance, use +^kosli attest in-toto^. The SBOM file is the only attachment: this command does not accept additional attachments, because two or more would be compressed together. @@ -74,6 +89,18 @@ kosli attest sbom yourDockerImageName \ --api-token yourAPIToken \ --org yourOrgName +# report an SBOM signed into a Sigstore bundle, verifying who signed it: +kosli attest sbom yourDockerImageName \ + --artifact-type docker \ + --name yourAttestationName \ + --sbom-file yourSbom.sigstore.json \ + --certificate-identity https://github.com/yourOrg/yourRepo/.github/workflows/build.yml@refs/heads/main \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --flow yourFlowName \ + --trail yourTrailName \ + --api-token yourAPIToken \ + --org yourOrgName + # report an SBOM about a trail: kosli attest sbom \ --name yourAttestationName \ @@ -136,6 +163,11 @@ func newAttestSbomCmd(out io.Writer) *cobra.Command { return ErrorBeforePrintingUsage(cmd, err.Error()) } + err = o.policy.Validate() + if err != nil { + return err + } + return ValidateRegistryFlags(cmd, o.fingerprintOptions) }, @@ -149,6 +181,7 @@ func newAttestSbomCmd(out io.Writer) *cobra.Command { ci := WhichCI() addAttestationFlags(cmd, o.CommonAttestationOptions, o.payload.CommonAttestationPayload, ci) cmd.Flags().StringVar(&o.sbomFilePath, "sbom-file", "", attestationSbomFileFlag) + addSignerPolicyFlags(cmd, &o.policy) // Required --sbom-file and mutually exclusive --attachments can never both be // satisfied, so the flag is hidden from help. Passing it still gets the error. _ = cmd.Flags().MarkHidden("attachments") @@ -213,20 +246,135 @@ func (o *attestSbomOptions) loadSbom() ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to read SBOM file [%s]: %s", o.sbomFilePath, err) } - fingerprint := fmt.Sprintf("%x", sha256.Sum256(content)) - data, err := sbom.ProcessSBOM(content) + document, evidence, err := o.unwrap(content) + if err != nil { + return nil, err + } + o.payload.AttestationData, err = sbomAttestationData(content, document) if err != nil { return nil, fmt.Errorf("failed to parse SBOM file [%s]: %s", o.sbomFilePath, err) } + // After CommonAttestationOptions.run, which assigns payload.Annotations + // wholesale from --annotate; called before it, both keys are discarded. + o.payload.Annotations = withAnnotations(o.payload.Annotations, sbomAnnotations(o.payload.AttestationData)) + if evidence != nil { + if err := o.recordVerification(evidence); err != nil { + return nil, err + } + } + return content, nil +} + +// unwrap returns the SBOM document content holds. A Sigstore bundle or DSSE +// envelope holds it as an in-toto attestation, which must be an SPDX or +// CycloneDX one about the artifact, when there is one, and is verified when a +// signer is pinned; evidence then says what was established, and is nil for a +// plain SBOM file. +func (o *attestSbomOptions) unwrap(content []byte) (document []byte, evidence *intoto.Evidence, err error) { + attestation, wrapped, err := intoto.ReadEnvelope(content) + if errors.Is(err, intoto.ErrSeveralDocuments) { + return nil, nil, fmt.Errorf("SBOM file [%s] %s. To record the SBOM from a file holding several attestations, such as an .intoto.jsonl file, use kosli attest in-toto --predicate-type sbom", o.sbomFilePath, err) + } + if err != nil { + return nil, nil, fmt.Errorf("failed to read SBOM file [%s]: %s", o.sbomFilePath, err) + } + if !wrapped { + if o.policy.PinsSigner() { + return nil, nil, fmt.Errorf("SBOM file [%s] is not a Sigstore bundle or DSSE envelope, so it has no signature to verify against --certificate-identity, --certificate-identity-regexp or --key", o.sbomFilePath) + } + return content, nil, nil + } + + document, err = intoto.SBOMDocument(attestation) + if err != nil { + return nil, nil, fmt.Errorf("SBOM file [%s]: %s", o.sbomFilePath, err) + } + if o.payload.ArtifactFingerprint != "" { + if err := attestation.RequireSubject(o.payload.ArtifactFingerprint); err != nil { + return nil, nil, fmt.Errorf("SBOM file [%s]: %s", o.sbomFilePath, err) + } + } + if !o.policy.PinsSigner() { + return document, &intoto.Evidence{}, nil + } + if !attestation.Signed() { + return nil, nil, fmt.Errorf("SBOM file [%s] is not signed, so it cannot be verified against --certificate-identity, --certificate-identity-regexp or --key", o.sbomFilePath) + } + evidence, err = intoto.NewVerifier(o.policy, intotoTrustRoots()).Verify(attestation, o.payload.ArtifactFingerprint) + if err != nil { + return nil, nil, fmt.Errorf("SBOM file [%s]: %s", o.sbomFilePath, err) + } + return document, evidence, nil +} + +// recordVerification annotates the record with what verifying the +// attestation established, and links its transparency log entry. These keys +// are reserved only for an SBOM that arrives in an attestation, so a plain +// SBOM file can still be annotated with them. +func (o *attestSbomOptions) recordVerification(evidence *intoto.Evidence) error { + for _, reserved := range []string{verifiedAnnotation, signerIdentityAnnotation, signerIssuerAnnotation, + logIndexAnnotation, integratedTimeAnnotation, signedTimestampAnnotation} { + if _, taken := o.annotations[reserved]; taken { + return fmt.Errorf("annotation key '%s' is set by this command from the attestation and cannot be provided with --annotate", reserved) + } + } + o.payload.Annotations = withAnnotations(o.payload.Annotations, verificationAnnotations(evidence)) + + if evidence.LogEntryURL == "" { + return nil + } + if _, taken := o.externalURLs[attestationExternalURL]; taken { + return fmt.Errorf("external URL label '%s' is set by this command from the attestation and cannot be provided with --external-url", attestationExternalURL) + } + if o.payload.ExternalURLs == nil { + o.payload.ExternalURLs = map[string]*URLInfo{} + } + o.payload.ExternalURLs[attestationExternalURL] = &URLInfo{Href: evidence.LogEntryURL, Fingerprint: evidence.RawSHA256} + return nil +} - o.payload.AttestationData = SbomAttestationData{ +// sbomAttestationData summarises document, the SBOM itself, and fingerprints +// attached, the bytes uploaded with the attestation, so the recorded checksum +// is one a reader can check against the attachment. The two are the same +// bytes unless the SBOM arrives inside an attestation bundle, which is then +// what is attached. +func sbomAttestationData(attached, document []byte) (SbomAttestationData, error) { + data, err := sbom.ProcessSBOM(document) + if err != nil { + return SbomAttestationData{}, err + } + return SbomAttestationData{ Format: data.Format, - OriginalFingerprint: fingerprint, + OriginalFingerprint: fmt.Sprintf("%x", sha256.Sum256(attached)), Document: data.Document, + }, nil +} + +// sbomAnnotations records what the SBOM said about itself where a reader sees +// it on the trail page. The same two values are carried inside +// attestation_data. +func sbomAnnotations(data SbomAttestationData) map[string]string { + return map[string]string{ + sbomFormatAnnotation: data.Format, + sbomSha256Annotation: data.OriginalFingerprint, } - o.annotate(data.Format, fingerprint) - return content, nil +} + +// withAnnotations returns base with extra added, in a new map. base is often +// the --annotate flag's own map -- processAnnotations returns its argument -- +// so writing into it would put derived keys into the map a reserved-key check +// reads, and a second run of the same options would be refused for a key +// nobody supplied. +func withAnnotations(base, extra map[string]string) map[string]string { + merged := make(map[string]string, len(base)+len(extra)) + for key, value := range base { + merged[key] = value + } + for key, value := range extra { + merged[key] = value + } + return merged } // rejectAttachments refuses --attachments however it was set. Two or more @@ -275,24 +423,3 @@ func (o *attestSbomOptions) rejectReservedAnnotations() error { } return nil } - -// annotate records what the file said about itself where a reader sees it on -// the trail page. The same two values are carried inside attestation_data. -// -// It must run after CommonAttestationOptions.run, which assigns -// payload.Annotations wholesale from the --annotate flag. Called before that, -// both keys are discarded. -// -// It builds a new map rather than writing into that one. The two are the same -// map -- processAnnotations returns its argument -- so writing would put these -// keys into the --annotate map that rejectReservedAnnotations reads, and a -// second run of the same options would be refused for a key nobody supplied. -func (o *attestSbomOptions) annotate(format, fingerprint string) { - merged := make(map[string]string, len(o.payload.Annotations)+2) - for key, value := range o.payload.Annotations { - merged[key] = value - } - merged[sbomFormatAnnotation] = format - merged[sbomSha256Annotation] = fingerprint - o.payload.Annotations = merged -} diff --git a/cmd/kosli/attestSbom_test.go b/cmd/kosli/attestSbom_test.go index 729604aea..a2345f86c 100644 --- a/cmd/kosli/attestSbom_test.go +++ b/cmd/kosli/attestSbom_test.go @@ -7,6 +7,8 @@ import ( "path/filepath" "testing" + "github.com/kosli-dev/cli/internal/intoto" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -67,6 +69,104 @@ func (suite *AttestSbomCommandTestSuite) TestAttestSbomBuildsTheRightRequest() { }) } +// An SBOM signed into an attestation is recorded with the attestation as the +// attachment, and verified when a signer is pinned. +func (suite *AttestSbomCommandTestSuite) TestAttestSbomUnwrapsAnAttestation() { + trustedRoot, err := root.NewTrustedRootFromPath(filepath.Join(intotoFixtures, "trusted_root.json")) + require.NoError(suite.T(), err) + saved := intotoTrustRoots + intotoTrustRoots = func() *intoto.TrustRoots { return intoto.FixedTrustRoots(trustedRoot) } + defer func() { intotoTrustRoots = saved }() + + bundle := filepath.Join(intotoFixtures, "spdx-sbom.sigstore.json") + bundleSHA256 := fixtureSHA256(suite.T(), "spdx-sbom.sigstore.json") + envelope := filepath.Join(intotoFixtures, "unsigned-spdx-sbom.dsse.json") + command := func(flags string) string { + return "attest sbom --name my-sbom " + flags + suite.defaultKosliArguments + } + + runTestCmd(suite.T(), []cmdTestCase{ + { + name: "reads the SBOM from a Sigstore bundle and fingerprints the bundle it attaches", + cmd: command("--dry-run --sbom-file " + bundle), + goldenRegex: `(?s)"filename": "spdx-sbom.sigstore.json".*"sbom_sha256": "` + bundleSHA256 + `",\s+"verified": "false".*"format": "spdx-2\.3",\s+"original_fingerprint": "` + bundleSHA256 + `"`, + }, + { + name: "verifies the bundle against the pinned signer and records the evidence", + cmd: command("--dry-run --sbom-file " + bundle + " " + intotoSigner), + goldenRegex: `(?s)"external_urls": \{\s+"attestation": \{\s+"href": "` + regexpQuote(intotoLogEntryURL) + `",\s+"fingerprint": "` + bundleSHA256 + `".*"signer_identity": "https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main".*"verified": "true"`, + }, + { + name: "checks the subject when the SBOM is about an artifact", + cmd: command("--dry-run --sbom-file " + bundle + " " + intotoSigner + " --fingerprint " + intotoDigest), + goldenRegex: `"verified": "true"`, + }, + { + wantError: true, + name: "refuses a verified SBOM about another artifact", + cmd: command("--sbom-file " + bundle + " " + intotoSigner + " --fingerprint 11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8"), + golden: "Error: SBOM file [" + bundle + "]: the attestation does not list the artifact (sha256:11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8) as its subject\n", + }, + { + wantError: true, + name: "refuses a bundle signed by someone other than the pinned signer", + cmd: command("--sbom-file " + bundle + " --certificate-identity https://github.com/mallory/widget/.github/workflows/build.yml@refs/heads/main --certificate-oidc-issuer https://token.actions.githubusercontent.com"), + goldenRegex: `^Error: SBOM file \[.*spdx-sbom.sigstore.json\]: the attestation failed verification: `, + }, + { + name: "reads the SBOM from an unsigned DSSE envelope, marked unverified", + cmd: command("--dry-run --sbom-file " + envelope), + goldenRegex: `(?s)"verified": "false".*"format": "spdx-2\.3"`, + }, + { + name: "accepts an unsigned SBOM about the artifact", + cmd: command("--dry-run --sbom-file " + envelope + " --fingerprint " + intotoDigest), + goldenRegex: `"verified": "false"`, + }, + { + wantError: true, + name: "refuses an unsigned SBOM about another artifact, though no signer is pinned", + cmd: command("--sbom-file " + envelope + " --fingerprint 11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8"), + golden: "Error: SBOM file [" + envelope + "]: the attestation does not list the artifact (sha256:11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8) as its subject\n", + }, + { + wantError: true, + name: "refuses to verify an unsigned DSSE envelope", + cmd: command("--sbom-file " + envelope + " " + intotoSigner), + golden: "Error: SBOM file [" + envelope + "] is not signed, so it cannot be verified against --certificate-identity, --certificate-identity-regexp or --key\n", + }, + { + wantError: true, + name: "points a file holding several attestations to attest in-toto", + cmd: command("--sbom-file " + filepath.Join(intotoFixtures, "attestations.intoto.jsonl")), + golden: "Error: SBOM file [" + filepath.Join(intotoFixtures, "attestations.intoto.jsonl") + "] holds several JSON documents, not a single bundle or envelope. To record the SBOM from a file holding several attestations, such as an .intoto.jsonl file, use kosli attest in-toto --predicate-type sbom\n", + }, + { + wantError: true, + name: "refuses an attestation that is not an SBOM", + cmd: command("--sbom-file " + filepath.Join(intotoFixtures, "github-provenance.sigstore.json")), + golden: "Error: SBOM file [" + filepath.Join(intotoFixtures, "github-provenance.sigstore.json") + "]: the attestation is https://slsa.dev/provenance/v1, not an SPDX or CycloneDX SBOM\n", + }, + { + wantError: true, + name: "refuses to verify a plain SBOM file, which has no signature", + cmd: command("--sbom-file testdata/sbom/cyclonedx.json " + intotoSigner), + golden: "Error: SBOM file [testdata/sbom/cyclonedx.json] is not a Sigstore bundle or DSSE envelope, so it has no signature to verify against --certificate-identity, --certificate-identity-regexp or --key\n", + }, + { + name: "still lets a plain SBOM file be annotated with a key the verification would set", + cmd: command("--dry-run --sbom-file testdata/sbom/cyclonedx.json --annotate verified=by-hand"), + goldenRegex: `"verified": "by-hand"`, + }, + { + wantError: true, + name: "reserves the verification annotations for an SBOM in an attestation", + cmd: command("--sbom-file " + bundle + " --annotate verified=by-hand"), + golden: "Error: annotation key 'verified' is set by this command from the attestation and cannot be provided with --annotate\n", + }, + }) +} + func (suite *AttestSbomCommandTestSuite) TestAttestSbomRejectsBadInput() { tests := []cmdTestCase{ { diff --git a/cmd/kosli/openapiContract_test.go b/cmd/kosli/openapiContract_test.go index fc4a272d9..ec74e9e16 100644 --- a/cmd/kosli/openapiContract_test.go +++ b/cmd/kosli/openapiContract_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/kosli-dev/cli/internal/gitview" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -67,6 +68,7 @@ func (suite *OpenAPIContractTestSuite) TestPayloadsMatchSchema() { registry := []driftCase{ {name: "create control", payload: ControlPayload{}, component: "ControlPostInput"}, {name: "create environment", payload: CreateEnvironmentPayload{}, component: "CreateEnvironmentPutInput"}, + {name: "git commit info", payload: gitview.BasicCommitInfo{}, component: "GitCommit"}, } for _, c := range registry { diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index afc235146..832933e54 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -337,7 +337,22 @@ Paths the list already matches stay excluded whatever is later added there, so k newComplianceStatusFlag = "The new compliance status to set on the attestation." originalAttestationTypeFlag = "The original attestation type being overridden (e.g. generic, snyk, junit, sonar, jira, pull_request, custom)." attestationDecisionControlFlag = "The control identifier being evaluated (e.g. RCTL-043)." - attestationSbomFileFlag = "The path to the SBOM file. CycloneDX (JSON, XML) and SPDX (JSON, tag-value) are supported." + attestationSbomFileFlag = "The path to the SBOM file. CycloneDX (JSON, XML) and SPDX (JSON, tag-value) are supported, also inside an in-toto attestation in a Sigstore bundle or DSSE envelope." + intotoSourceFlag = "[defaulted] Where to read the attestation from. Only 'bundle' is supported for now: the file given with --bundle." + intotoBundleFlag = "[conditional] The file holding the attestation, with --source bundle: a Sigstore bundle (.sigstore.json), a DSSE envelope, an in-toto statement, or an .intoto.jsonl file with one of these per line." + intotoPredicateTypeFlag = "The kind of attestation to record: provenance (SLSA provenance v1 or v0.2) or sbom (SPDX or CycloneDX)." + intotoNameFlag = "The name of the record as declared in the flow or trail yaml template: the artifact's name for provenance, the attestation's name for sbom." + certificateIdentityFlag = "[conditional] The identity a signed attestation's certificate must name, such as a GitHub workflow URL. Use with --certificate-oidc-issuer." + certificateIdentityRegexpFlag = "[conditional] A regular expression the identity in a signed attestation's certificate must match. Use instead of --certificate-identity, with --certificate-oidc-issuer." + certificateOIDCIssuerFlag = "[conditional] The OIDC issuer a signed attestation's certificate must name, such as https://token.actions.githubusercontent.com." + signerKeyFlag = "[conditional] The path to a PEM public key, to verify an attestation signed with a key rather than a certificate." + allowUnsignedFlag = "[optional] Record an unsigned attestation, marked as unverified. Without it, an unsigned attestation is refused." + intotoBuildURLFlag = "[optional] The build URL to record when the provenance does not say. Provenance only." + intotoCommitURLFlag = "[optional] The commit URL to record when the provenance does not say. Provenance only." + intotoRepoProviderFlag = "[optional] The provider of the repository the provenance names, when its host is not github.com, gitlab.com or bitbucket.org. One of: " + repoProviderList + ". For a GitHub Enterprise host, also give --github-base-url to look the commit up there." + intotoRepoRootFlag = "[defaulted] The git checkout to read the commit's details from when they cannot be looked up. Provenance only." + intotoGithubTokenFlag = "[optional] A GitHub token, to look up the commit's message, author and time when the provenance names a GitHub repository." + intotoGithubBaseURLFlag = "[optional] The GitHub Enterprise Server to look the commit up on, such as https://github.example.com, when the provenance names a repository there. Without it the commit is looked up on github.com, never at a host taken from the provenance." excludeScalingFlag = "[optional] Exclude scaling events for snapshots. Snapshots with scaling changes will not result in new environment records." includeScalingFlag = "[optional] Include scaling events for snapshots. Snapshots with scaling changes will result in new environment records." includedEnvironments = "[optional] Comma separated list of environments to include in logical environment" diff --git a/cmd/kosli/testdata/empty-flag-audit-coverage.json b/cmd/kosli/testdata/empty-flag-audit-coverage.json index 3efef2764..83f6c251f 100644 --- a/cmd/kosli/testdata/empty-flag-audit-coverage.json +++ b/cmd/kosli/testdata/empty-flag-audit-coverage.json @@ -192,6 +192,35 @@ "trail": "string", "user-data": "string" }, + "attest in-toto": { + "allow-unsigned": "bool", + "annotate": "stringToString", + "artifact-type": "string", + "build-url": "string", + "bundle": "string", + "certificate-identity": "string", + "certificate-identity-regexp": "string", + "certificate-oidc-issuer": "string", + "commit-url": "string", + "dry-run": "bool", + "exclude": "stringSlice", + "external-fingerprint": "stringToString", + "external-url": "stringToString", + "fingerprint": "string", + "flow": "string", + "github-base-url": "string", + "github-token": "string", + "key": "string", + "name": "string", + "predicate-type": "string", + "registry-password": "string", + "registry-provider": "string", + "registry-username": "string", + "repo-provider": "string", + "repo-root": "string", + "source": "string", + "trail": "string" + }, "attest jira": { "annotate": "stringToString", "artifact-type": "string", @@ -410,6 +439,9 @@ "annotate": "stringToString", "artifact-type": "string", "attachments": "stringSlice", + "certificate-identity": "string", + "certificate-identity-regexp": "string", + "certificate-oidc-issuer": "string", "commit": "string", "description": "string", "dry-run": "bool", @@ -418,6 +450,7 @@ "external-url": "stringToString", "fingerprint": "string", "flow": "string", + "key": "string", "name": "string", "origin-url": "string", "redact-commit-info": "stringSlice", diff --git a/go.mod b/go.mod index 87f4ea2e2..7e3214854 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/google/uuid v1.6.0 github.com/googleapis/gax-go/v2 v2.27.0 github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/in-toto/attestation v1.2.0 github.com/joshdk/go-junit v1.0.0 github.com/mattn/go-shellwords v1.0.16 github.com/maxcnunes/httpfake v1.2.4 @@ -40,7 +41,11 @@ require ( github.com/otiai10/copy v1.14.1 github.com/owenrumney/go-sarif/v2 v2.3.3 github.com/rjeczalik/notify v0.9.3 + github.com/secure-systems-lab/go-securesystemslib v0.11.1 github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf + github.com/sigstore/protobuf-specs v0.5.2 + github.com/sigstore/sigstore v1.11.1 + github.com/sigstore/sigstore-go v1.4.0 github.com/spdx/tools-golang v0.5.7 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.1.0 @@ -80,9 +85,10 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.10.1 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.2.0 // indirect + github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/agnivade/levenshtein v1.2.2 // indirect github.com/anchore/go-struct-converter v0.1.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.27 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.9 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.12 // indirect @@ -97,6 +103,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.44.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.52.0 // indirect github.com/aws/smithy-go v1.28.9 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -104,10 +111,13 @@ require ( github.com/containers/ocicrypt v1.2.1 // indirect github.com/containers/storage v1.59.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.7.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/digitorus/pkcs7 v0.0.0-20260821105541-05f79448fa77 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.3.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect @@ -124,8 +134,15 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v1.0.0 // indirect + github.com/go-openapi/errors v0.22.9 // indirect github.com/go-openapi/jsonpointer v1.0.1 // indirect - github.com/go-openapi/jsonreference v1.0.1 // indirect + github.com/go-openapi/jsonreference v1.0.2 // indirect + github.com/go-openapi/loads v0.25.3 // indirect + github.com/go-openapi/runtime v0.33.3 // indirect + github.com/go-openapi/runtime/server-middleware v0.33.3 // indirect + github.com/go-openapi/spec v1.0.1 // indirect + github.com/go-openapi/strfmt v0.27.4 // indirect github.com/go-openapi/swag v0.29.2 // indirect github.com/go-openapi/swag/cmdutils v0.29.2 // indirect github.com/go-openapi/swag/conv v0.29.2 // indirect @@ -138,6 +155,7 @@ require ( github.com/go-openapi/swag/stringutils v0.29.2 // indirect github.com/go-openapi/swag/typeutils v0.29.2 // indirect github.com/go-openapi/swag/yamlutils v0.29.2 // indirect + github.com/go-openapi/validate v1.0.0 // indirect github.com/go-playground/locales v0.14.2 // indirect github.com/go-playground/universal-translator v0.18.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect @@ -147,14 +165,19 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/certificate-transparency-go v1.4.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-containerregistry v0.22.1 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.11 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.25 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -174,19 +197,25 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/onsi/gomega v1.40.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/otiai10/mint v1.6.3 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pjbgf/sha1cd v0.7.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sassoftware/relic/v8 v8.2.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/sergi/go-diff v1.4.0 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/sigstore/rekor v1.5.5 // indirect + github.com/sigstore/rekor-tiles/v2 v2.4.0 // indirect + github.com/sigstore/timestamp-authority/v2 v2.2.0 // indirect github.com/sirupsen/logrus v1.10.2 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -194,6 +223,10 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/theupdateframework/go-tuf v0.7.0 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.2 // indirect + github.com/transparency-dev/formats v0.1.1 // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect github.com/trivago/tgo v1.0.7 // indirect github.com/valyala/fastjson v1.6.10 // indirect github.com/vektah/gqlparser/v2 v2.5.64 // indirect @@ -202,8 +235,9 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 // indirect go.opentelemetry.io/otel v1.47.0 // indirect go.opentelemetry.io/otel/log v1.47.0 // indirect @@ -212,6 +246,7 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.6 // indirect golang.org/x/crypto v0.58.0 // indirect + golang.org/x/mod v0.43.0 // indirect golang.org/x/net v0.62.0 // indirect golang.org/x/sys v0.50.0 // indirect golang.org/x/text v0.43.0 // indirect diff --git a/go.sum b/go.sum index f511cc910..09ca45ac1 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ cloud.google.com/go/compute/metadata v0.10.0 h1:pyKMUQSwchgkIBBJGdILqQbs/BNJXqwS cloud.google.com/go/compute/metadata v0.10.0/go.mod h1:rGFHRrIif570kSibjFTMbt6/4/tzgJWFGI/HVol4GIk= cloud.google.com/go/iam v1.14.0 h1:YRIB1/4xxwJ+NfI6RQr1WaOoHHeV0MizJEj940nBXJA= cloud.google.com/go/iam v1.14.0/go.mod h1:JTPZfbHdGXJrOYREtkRoFzCDGaBdbA7ILLKO/DDuav0= +cloud.google.com/go/kms v1.33.0 h1:pG0X78m212b2pv9N4fdMoUO69LuZGQ9kSvn8sHBOFAo= +cloud.google.com/go/kms v1.33.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA= cloud.google.com/go/longrunning v1.5.0 h1:aGonu6LHQl38DyozLs2jxGieI4Ixk+7GTnmHVcHt8zA= cloud.google.com/go/longrunning v1.5.0/go.mod h1:vjM1U85ZQuTv8lJoUBSA44HLWvM6BBFeaSJYGFF1Q00= cloud.google.com/go/orgpolicy v1.20.0 h1:kpVcE/OsC5aAzHCsAiuQSg3+s6ILzgPTuPZyS7n7ejA= @@ -24,6 +26,12 @@ cloud.google.com/go/run v1.25.0 h1:ddLJB4WUchBI9OtHly+MdwoRmOJuwQdbQ8hBrjr8T/c= cloud.google.com/go/run v1.25.0/go.mod h1:qcFpJvl+EzRKP8XNtVzkD80FdlrihiWZ2CchMNONa8E= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e h1:VsUbObBMxXlc23Eb9VeeJYE4jvTs87qa5RqSN2U5FJU= +filippo.io/mldsa v0.0.0-20260215214346-43d0283efc3e/go.mod h1:32qQ5yj3R24Eu03iWFWchdC3OB653wPvoepWejkefbY= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.24.0 h1:5H6YRLGMoC8z5aXpxqcD6FfswI8mwoVNHSecvIoazt4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.24.0/go.mod h1:Sk63W6X/TElfcE2BknMYg0Q9JuCpebgjnHcbs5md4NM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.15.0 h1:w3gDUJlJ0BfBsHH02DCBDwbkmMSXJqBj6nBOCo9iocY= @@ -36,6 +44,10 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.13.0 h1:Terj1CMW4+NY5CpWjSRkqL github.com/Azure/azure-sdk-for-go/sdk/internal v1.13.0/go.mod h1:7Ut2Qw9vcwpLW5Q4MBy43uSxXS7tK2yjs94OJ6j5thE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0 h1:JI8PcWOImyvIUEZ0Bbmfe05FOlWkMi2KhjG+cAKaUms= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0/go.mod h1:nJLFPGJkyKfDDyJiPuHIXsCi/gpJkm07EvRgiX7SGlI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 h1:MaKvxE6D0KkjOg6Wd9M00iqP5PR0kUxCfiezes4JweM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0/go.mod h1:i2h9fsTFKZorh8RdV2IcSUf/Qj98GlTkrTvUbX/s8as= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.10.1 h1:0O6j18nQDoIff6/mtosT5LNU2WehEwp+LqfX6699oqY= @@ -49,8 +61,8 @@ github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= -github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/agnivade/levenshtein v1.2.2 h1:fI8RgiztYX1Nn5pBHHqQ9EMCCklhrmXqPRIedFmx5M4= github.com/agnivade/levenshtein v1.2.2/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= @@ -64,6 +76,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go-v2 v1.48.0 h1:tFU7EJVm7MfyaXLsXdYTZ0Vd6vwt9OljCyqq0gqk0Mc= github.com/aws/aws-sdk-go-v2 v1.48.0/go.mod h1:oeKU5p+TQBnpeLmU42WZLWvzv+ORLn+74n4QVLnm/Z0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.27 h1:3dMP9eEmHVosFN1QiD24IJBKVY8m6iUfVYXqRCT118Q= @@ -92,6 +106,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.12 h1:wzAqMZ2O github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.12/go.mod h1:/0TfyruByHsOlphzP26xDTRe2DKD0J1I1Uyy2TyP8EI= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.12 h1:H0IaQayT3TLiGIHiNnleQZTbndDlx7LGZ0NWZ5mDwjA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.12/go.mod h1:YuJJNxaOjnknLIrscBmkLhIpnmg9LzHd25sQRKUboGU= +github.com/aws/aws-sdk-go-v2/service/kms v1.61.4 h1:vW7497x7W6ucLJAqDt96jIdO4+IWLMB3p98UubwjdqM= +github.com/aws/aws-sdk-go-v2/service/kms v1.61.4/go.mod h1:Vz59N7x42VGLXZ5dGL9b10QhrU4GQv7MqnO7wc8bl9U= github.com/aws/aws-sdk-go-v2/service/lambda v1.114.4 h1:m9KfA2ojf6w77HjOrl8kpiNHAHz+sar1M0yDzEIxMEo= github.com/aws/aws-sdk-go-v2/service/lambda v1.114.4/go.mod h1:Y/PfQ6QmryXcdj5kOOCMUYFYCiDvwuYpjCJJE76DEKs= github.com/aws/aws-sdk-go-v2/service/s3 v1.114.10 h1:WBJb2VxRzzKnuN94YWqZPiu5a5zc3IleFqgZnmVCyvU= @@ -110,12 +126,18 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -130,9 +152,13 @@ github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpV github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= +github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM= +github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= @@ -149,10 +175,15 @@ github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINA github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20260821105541-05f79448fa77 h1:ellGgwdFy8F+Fn/AElhKzTysK1/Js7S9ZmMnSu2gSS0= +github.com/digitorus/pkcs7 v0.0.0-20260821105541-05f79448fa77/go.mod h1:OQvyvldkvDCxGsUPLSibXqM4B/Pz5RaXQP4CPo5LXs4= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= -github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= +github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= @@ -198,6 +229,8 @@ github.com/gabriel-vasile/mimetype v1.4.16 h1:5izNFuvA37st1dDRAcA7TtmngBmRgiIPDm github.com/gabriel-vasile/mimetype v1.4.16/go.mod h1:3oFFwxX8kTklZwzIl2aQEDZT0lKY8xH92ST88bAwba4= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-chi/chi/v5 v5.5.0 h1:xdKQ1dJmVJsmgahrXSKhUx8NyVPzpwdlyrrQFpMPJgg= +github.com/go-chi/chi/v5 v5.5.0/go.mod h1:d1IG7GtKPSjPyBP365PH8sAGVHk3qfB/IhjFNRH1wWM= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE= @@ -206,15 +239,31 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4= github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4= +github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4= +github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v1.0.0 h1:sNvbAGCJqUTqIAodr9IVqJMmuZas3YS9ms1dGK9yiJ4= +github.com/go-openapi/analysis v1.0.0/go.mod h1:NhYjJ57fnE+bcE7UwrJyMkhWA3Dfz7TdiBfTVAnos4Y= +github.com/go-openapi/errors v0.22.9 h1:HI9+SyVYiRzyeBQGv0CbWbQa+u2S0nny7G7AhY+UMiM= +github.com/go-openapi/errors v0.22.9/go.mod h1:R73q66sXP2smzAGgFOI4nqtusRK2xN5pYCjsij+KX58= github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg= github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY= -github.com/go-openapi/jsonreference v1.0.1 h1:4zJ7AmYDKNmD3aSpfPnFNCFA5E80/xMHUNKgydaLh38= -github.com/go-openapi/jsonreference v1.0.1/go.mod h1:dYplQXa6p5lXprLcJ8LE2iU7vNpXsAHDQ5ZAgL+Qx3A= +github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc= +github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs= +github.com/go-openapi/loads v0.25.3 h1:V+jKy/thXWdLJUuYC8sZX2dICyAa8M3DokF70jj34P0= +github.com/go-openapi/loads v0.25.3/go.mod h1:LgLyCSOLBL2Qnj0Ps1oo2YH8jZoEKsMeVZCc+ALIxFo= +github.com/go-openapi/runtime v0.33.3 h1:qVgTDaFH+pzO8UqlwYNVmx2DD+nwVK3rB65V29ednD0= +github.com/go-openapi/runtime v0.33.3/go.mod h1:57crQFXXnCtT68LuAd+NOELX5tSF4ZE1GFW3Z+r/KCk= +github.com/go-openapi/runtime/server-middleware v0.33.3 h1:Td5UBla/J/KCjlsD0tmZYWQWxnT4gHpfXbueLQv9Umw= +github.com/go-openapi/runtime/server-middleware v0.33.3/go.mod h1:dopwrwqIqXe4xWtovDZkkXJzYqKw+Ybmy7G/hIDUWKY= +github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk= +github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8= +github.com/go-openapi/strfmt v0.27.4 h1:E1xsvHb8R9vsuYru4TP2PVVTv9AIgSFAUACUZrFQfCI= +github.com/go-openapi/strfmt v0.27.4/go.mod h1:SCOff81PPhU26acBiW96jHaFycEbT2nkQRvojM+V2z0= github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4= github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo= github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro= @@ -241,10 +290,12 @@ github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONK github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE= github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM= github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ= -github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs= -github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs= -github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8= -github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-openapi/testify/enable/yaml/v2 v2.8.0 h1:dvOQNZ2ovOdogn902NYq7i6jUI9Dma4qyjTXABY5YTY= +github.com/go-openapi/testify/enable/yaml/v2 v2.8.0/go.mod h1:3VwZZUT6nWcAjhIr2RypXxlsxd3mPPtXh3T8z65aRlQ= +github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho= +github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak= +github.com/go-openapi/validate v1.0.0 h1:dFsYCLVUQUL6Vi2lQSexgwmCXDuHe7eWRDhQxkE+xYA= +github.com/go-openapi/validate v1.0.0/go.mod h1:wwXGRqMQzOZ7PCqBcgNk+DD9+Cacnxv7we5T0M/eA3Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.2 h1:d8UmcrM6Nip0hfGZKLGpAvZH37XB4TS0xzK9B56YNCY= @@ -253,6 +304,8 @@ github.com/go-playground/universal-translator v0.18.2 h1:LCsMLC9RzmbUMNUPVYD15dm github.com/go-playground/universal-translator v0.18.2/go.mod h1:67VZIMp5lQpDWlnStOct22q1bkdJGJqHghbOtmkawxk= github.com/go-playground/validator/v10 v10.30.6 h1:E+mymlPNRzAMNz1FShrRvLeGWcprF5w305eLLgWieuo= github.com/go-playground/validator/v10 v10.30.6/go.mod h1:jPLL8SKCXGe/g87QmEyNw3NO28nmGt5GCnZnufm7rj4= +github.com/go-test/deep v1.1.2 h1:JUuyv17rKrqYfLFnP9A4Yh6hw0635ugB93bMg2bwuDQ= +github.com/go-test/deep v1.1.2/go.mod h1:lwVFiFFWPjydTsV3xGJ/YTQsnWwi8zxBFM+gOVcwINE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v1.1.0 h1:CS8tIr7+5fu50N+tKsQy3osOU5jmhjHv0/ildcKAn2Q= @@ -271,6 +324,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.4.0 h1:78uNaR2knzCoKKXXDSAGVkz9XPkDg5xXJRP9tP6+rr0= +github.com/google/certificate-transparency-go v1.4.0/go.mod h1:iR17ZgSaXRzSa5qvjFl8TnVD5h8ky2JMVio+dzoKMgA= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -279,15 +334,21 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84= +github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= github.com/google/go-github/v93 v93.0.0 h1:qElXPLv+XbSz1qrLdWyb0t3gSYGsWBTzr9T4/PZTCfk= github.com/google/go-github/v93 v93.0.0/go.mod h1:l8I3rt4/mdjANKpg4Y84LEy9HJQCetUkZ8WTl2bPU8M= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.11 h1:7v1V1TraNGP63WptsAkNqr2w8jgm4vM6A6qx6Lsw9iM= github.com/google/s2a-go v0.1.11/go.mod h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/trillian v1.8.0 h1:AnbjOFFMQyvqpPAARj97CKEXmoSpCy2hZw0T7Y1Y0BA= +github.com/google/trillian v1.8.0/go.mod h1:E9eOXWvvyzIMLXLBgYmwklSl2RORBqGX1hDJkEoHdRc= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -299,16 +360,47 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 h1:9ZJYjPEJpcleIKcqysXOo14NLQYwu23fzmKRFpIjPjc= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4/go.mod h1:0xydIeg2omQ9DH6zYMP24Kk57793rnLUvP8fwdbFvz8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0 h1:QMILzF8BA7xftE4zVORkRrlv08m12St3csK4RsyRgRs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.32.0/go.mod h1:+ZQ6iUL8+ATyxGDQNuRKpNwJFdGDUi0e7E1QL9MvI7U= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/in-toto/attestation v1.2.0 h1:aPRUZ3azbqD7yEBD5fP3TD8Dszf+YHo284SOcpahjQk= +github.com/in-toto/attestation v1.2.0/go.mod h1:r79G45gOmzPismgObLSL+rZTFxUgZLOQJI6LofTZgXk= +github.com/in-toto/in-toto-golang v0.11.0 h1:nfidMYBFx+E0lnmX5KUnN2Pdm8zdNKal1ayjJuzzRoA= +github.com/in-toto/in-toto-golang v0.11.0/go.mod h1:u3PjTnwFKjp5a1YCcw8SJg0G+tMeKfVoWsWeFMDCMtw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8= +github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU= +github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= +github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= @@ -359,6 +451,10 @@ github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHS github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/moby v28.5.2+incompatible h1:hIn6qcenb3JY1E3STwqEbBvJ8bha+u1LpqjX4CBvNCk= @@ -381,6 +477,10 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= +github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/open-policy-agent/opa v1.22.3 h1:d46phjoHHFSl8F2bC8nT8wFxuGEvZRvs8FhYWFqsJ2w= @@ -398,10 +498,11 @@ github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAq github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA= github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -428,14 +529,42 @@ github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWb github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sassoftware/relic/v8 v8.2.0 h1:9/L4S4I6an/JsPNhmcTpqfiOIsLb/iJaePZD1xR+ulE= +github.com/sassoftware/relic/v8 v8.2.0/go.mod h1:pZy7hLT9WCOKPonV8G/fplvtBLOZd6/kWtKqeHR6nKc= +github.com/secure-systems-lab/go-securesystemslib v0.11.1 h1:ayahDPjfSIqKegyt5YVGEvQ7SAi72GHXTy+b2YjUd5w= +github.com/secure-systems-lab/go-securesystemslib v0.11.1/go.mod h1:UyOjhoZLi76ir63u1ptubEevVe6dlNPmNxwgVvKqFFY= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf h1:o1uxfymjZ7jZ4MsgCErcwWGtVKSiNAXtS59Lhs6uI/g= github.com/shurcooL/graphql v0.0.0-20240915155400-7ee5256398cf/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/sigstore/protobuf-specs v0.5.2 h1:RSWWUY8QrVTxbYH00jY/jg2e7YnjzrpwP+PeHTMll0E= +github.com/sigstore/protobuf-specs v0.5.2/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/rekor v1.5.5 h1:nZKTBT0uRlKaFW4PafFIE7F5H26PInFF2qOlWouKYYA= +github.com/sigstore/rekor v1.5.5/go.mod h1:A0e/gTyJ5qCQn6/Zz2T0+TE+W3akuaLF5O0HJ4ogjgA= +github.com/sigstore/rekor-tiles/v2 v2.4.0 h1:Q5vQvZPrHXBzz5/WuF4q7D9RGUm/JzXJQhNGpd4G5Pw= +github.com/sigstore/rekor-tiles/v2 v2.4.0/go.mod h1:CwJCXjoMBNFKctuPZToeQytli9Nfx1Tvzm09AyJHJNM= +github.com/sigstore/sigstore v1.11.1 h1:kPqGeoOk3IGCJH76ssgX5PKMm8mbHNnM9fhKLDc6M8E= +github.com/sigstore/sigstore v1.11.1/go.mod h1:0jewOSMygmxdOaBIOhD1teBSIleg7RExQ1baacNrv4A= +github.com/sigstore/sigstore-go v1.4.0 h1:CGdFdZLhYSc42yYN0+lL6yWh7v0z3S0i21v50lvSYKw= +github.com/sigstore/sigstore-go v1.4.0/go.mod h1:T2Csm9q65ZVCrTB8q1fP60wR9Z9Hf1286zBTd6pDG88= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.11.1 h1:Xw+W6YvW/bQ3zSAYK6hsOQplGz/X9yOvCGeQLfkNy4c= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.11.1/go.mod h1:agtjJMPuI6q2rBTM0XZiOFsZjlcZnTi7xkBFQt0NISY= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.11.1 h1:UfIJOml1Y9uJwTy8cDbP7a6x/0By+fXIV7dyUHvEZlA= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.11.1/go.mod h1:xQ5RXq7EDorvH2+1cNQLXQUjXVlhz64q+CZf5Jol8FU= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.11.1 h1:Ab4K/QqBmlmOu5X05qLTeQnPblVqqzrC//F7J/imzoI= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.11.1/go.mod h1:db/U8Hp9wrarN12CE99SdDEef9FCsgyGTD2gaFRfep4= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.11.1 h1:WIgn5z+Wx9ZM8MPTZlMIJ5YXZq36VrcYXFfCCokMqGc= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.11.1/go.mod h1:byZFStGl4JcpWux75DD7gmpYyYjyy2BmkjsJhdSA8UE= +github.com/sigstore/timestamp-authority/v2 v2.2.0 h1:8q0R5iKss8YxbdoMM929G3djst/yXkdERTQavyLzBiQ= +github.com/sigstore/timestamp-authority/v2 v2.2.0/go.mod h1:KdNxJI1HURK3EUaOjy3S+dfqoia/Iv/DtcvSR1/R1TI= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= @@ -472,10 +601,26 @@ github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13 github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.4.2 h1:w7976/W8uTwlsegP5nRymlpjPgrwSh+AXUf85is6nJk= +github.com/theupdateframework/go-tuf/v2 v2.4.2/go.mod h1:JqBrIUnNLAaNq/8GmBcEMFWfAFBbqp/MkJEJseXKbks= +github.com/tink-crypto/tink-go-awskms/v3 v3.0.0 h1:XSohRhCkXAVI0iaCnWB/GS05TEmpnKurQmzaY1jzt3Y= +github.com/tink-crypto/tink-go-awskms/v3 v3.0.0/go.mod h1:+7MXsShLzVbSQ6dI0Pe4JuZM52jD1jQ1itAygd/MDsA= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.4.0 h1:xpI5pnEQ0erFCS3JmcEl7Blluo8ZieEPNOURqy9uD70= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.4.0/go.mod h1:wDDAhjfd1t4TjeJCSsmFK7CARPUs/ITi16ZZ8kmAaFQ= +github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0 h1:eXuNqgrcYelxU1MVikOJDP3wTS5lvihM4ntoAbAMfvs= +github.com/tink-crypto/tink-go-hcvault/v2 v2.5.0/go.mod h1:3RhcxAqek6xUlRFmJifvU4CYLZN60KMQdIKqpZAZJG0= +github.com/tink-crypto/tink-go/v2 v2.9.0 h1:uvjgS1IYDB4zhDK0Zow8ZFB73IjzMAoSqW0JkFnvkrc= +github.com/tink-crypto/tink-go/v2 v2.9.0/go.mod h1:aNXZeyxjQU9iqAeARRNmbESXUW6Mao1HRCCTY8B1TFM= +github.com/transparency-dev/formats v0.1.1 h1:4bVHJc+KdBgpA1OJD1yjI+g0i5Z1graCppTMH8lWKJI= +github.com/transparency-dev/formats v0.1.1/go.mod h1:qtZ8goRuJ8FTBG9c9+Bj0rn2rUG7eG/AUTkr+Aw3jFw= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= @@ -500,6 +645,8 @@ github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5 github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= @@ -507,8 +654,8 @@ gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0 h1:Tq+E65HaNbZTeqcLw6QUhsV+/EIWdkfUH2CQzcrlE5M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.72.0/go.mod h1:A+EVhDakAj1waJBPhmTszZaSr08ww8MRRKy1nB1rqck= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw= go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM= @@ -523,6 +670,14 @@ go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw= go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM= go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0= +go.step.sm/crypto v0.91.0 h1:0mN0DwVOvUuh7VbyTnxABZuAkxwak/Grp8Y/b4YaZDU= +go.step.sm/crypto v0.91.0/go.mod h1:NxxObRBymdbyXLoTCW5Ea6sLxuy5yI/xr9+hSBlbU/Q= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= @@ -567,8 +722,8 @@ golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= -golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.312.0 h1:c6EbM0/irOk8MemsYRO3ewSQWxVk2QYQJ/rYHXwEYh0= @@ -626,3 +781,5 @@ sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3C sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M= +software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/hack/empty-flag-audit/spec.json b/hack/empty-flag-audit/spec.json index 1959442f6..13723848a 100644 --- a/hack/empty-flag-audit/spec.json +++ b/hack/empty-flag-audit/spec.json @@ -4957,6 +4957,9 @@ "annotate", "artifact-type", "attachments", + "certificate-identity", + "certificate-identity-regexp", + "certificate-oidc-issuer", "commit", "description", "dry-run", @@ -4965,6 +4968,7 @@ "external-url", "fingerprint", "flow", + "key", "name", "origin-url", "redact-commit-info", @@ -4984,6 +4988,9 @@ "annotate": "probe=annotate", "artifact-type": "file", "attachments": "cmd/kosli/testdata/person-schema.json", + "certificate-identity": "https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main", + "certificate-identity-regexp": "^https://github.com/acme/widget/", + "certificate-oidc-issuer": "https://token.actions.githubusercontent.com", "commit": "HEAD", "description": "probe-description", "dry-run": "true", @@ -4992,6 +4999,7 @@ "external-url": "probe=http://example.com", "fingerprint": "1bef738d0bb1e690500f99a5b57d958caf3a5eb3e00d9012e1f4369fc6812e01", "flow": "{flow}", + "key": "internal/intoto/testdata/key.pub", "name": "{name}", "origin-url": "http://example.com", "redact-commit-info": "author", @@ -5095,5 +5103,78 @@ } ], "verify": [] + }, + "attest in-toto": { + "args": [], + "flags": { + "allow-unsigned": "true", + "bundle": "internal/intoto/testdata/statements/github-provenance.json", + "fingerprint": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8", + "flow": "{flow}", + "name": "{name}", + "predicate-type": "provenance", + "trail": "{trail}" + }, + "baseline_ok": true, + "baseline_exit": 0, + "baseline_output": "[warning] the details and branch of commit 4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70 are not recorded, only its SHA: they could not be looked up, and the git repository at --repo-root . does not hold the commit\nartifact artifact.bin was attested with fingerprint: e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8", + "flags_to_test": [ + "allow-unsigned", + "annotate", + "artifact-type", + "build-url", + "bundle", + "certificate-identity", + "certificate-identity-regexp", + "certificate-oidc-issuer", + "commit-url", + "dry-run", + "exclude", + "external-fingerprint", + "external-url", + "fingerprint", + "flow", + "github-base-url", + "github-token", + "key", + "name", + "predicate-type", + "registry-password", + "registry-provider", + "registry-username", + "repo-provider", + "repo-root", + "source", + "trail" + ], + "flag_values": { + "allow-unsigned": "true", + "annotate": "probe=annotate", + "artifact-type": "file", + "build-url": "http://example.com", + "bundle": "internal/intoto/testdata/statements/github-provenance.json", + "certificate-identity": "https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main", + "certificate-identity-regexp": "^https://github.com/acme/widget/", + "certificate-oidc-issuer": "https://token.actions.githubusercontent.com", + "commit-url": "http://example.com", + "dry-run": "true", + "exclude": "probe-exclude", + "external-fingerprint": "probe=1bef738d0bb1e690500f99a5b57d958caf3a5eb3e00d9012e1f4369fc6812e01", + "external-url": "probe=http://example.com", + "fingerprint": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8", + "flow": "{flow}", + "github-base-url": "http://example.com/", + "github-token": "probe-github-token", + "key": "internal/intoto/testdata/key.pub", + "name": "{name}", + "predicate-type": "provenance", + "registry-password": "probe-registry-password", + "registry-provider": "probe-registry-provider", + "registry-username": "probe-registry-username", + "repo-provider": "github", + "repo-root": ".", + "source": "bundle", + "trail": "{trail}" + } } } diff --git a/internal/github/commit.go b/internal/github/commit.go new file mode 100644 index 000000000..27c0ca2f3 --- /dev/null +++ b/internal/github/commit.go @@ -0,0 +1,53 @@ +package github + +import ( + "context" + "fmt" + "strings" + + "github.com/kosli-dev/cli/internal/types" +) + +// CommitFinder looks up commits through GitHub's REST API. An empty BaseURL +// means github.com; set it for GitHub Enterprise Server. +type CommitFinder struct { + Token string + BaseURL string + Debug bool +} + +// CommitBySHA returns the commit with sha in repository, named owner/name. +func (f CommitFinder) CommitBySHA(repository, sha string) (*types.Commit, error) { + owner, name, ok := strings.Cut(repository, "/") + if !ok || owner == "" || name == "" { + return nil, fmt.Errorf("GitHub repository %q is not in the form owner/name", repository) + } + ctx := context.Background() + client, err := NewGithubClientFromToken(ctx, f.Token, f.BaseURL, f.Debug) + if err != nil { + return nil, err + } + found, _, err := client.Repositories.GetCommit(ctx, owner, name, sha, nil) + if err != nil { + return nil, fmt.Errorf("failed to get commit %s from GitHub repository %s: %w", sha, repository, err) + } + + gitAuthor := found.GetCommit().GetAuthor() + // A commit with no author is recorded as "", not " <>". + author := "" + if gitAuthor.GetName() != "" || gitAuthor.GetEmail() != "" { + author = fmt.Sprintf("%s <%s>", gitAuthor.GetName(), gitAuthor.GetEmail()) + } + var timestamp int64 + if date := gitAuthor.GetDate(); !date.IsZero() { + timestamp = date.Unix() + } + return &types.Commit{ + SHA: found.GetSHA(), + Message: strings.TrimSpace(found.GetCommit().GetMessage()), + Author: author, + AuthorUsername: found.GetAuthor().GetLogin(), + Timestamp: timestamp, + URL: found.GetHTMLURL(), + }, nil +} diff --git a/internal/github/commit_test.go b/internal/github/commit_test.go new file mode 100644 index 000000000..3bc61b896 --- /dev/null +++ b/internal/github/commit_test.go @@ -0,0 +1,64 @@ +package github + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/kosli-dev/cli/internal/types" + "github.com/stretchr/testify/require" +) + +const lookedUpSHA = "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70" + +var _ types.CommitFinder = CommitFinder{} + +func TestCommitBySHA(t *testing.T) { + t.Run("returns the commit's message, author, time and page", func(t *testing.T) { + var gotAuth string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/api/v3/repos/acme/widget/commits/"+lookedUpSHA, r.URL.Path) + gotAuth = r.Header.Get("Authorization") + _, _ = fmt.Fprintf(w, `{ + "sha": %q, + "html_url": "https://github.com/acme/widget/commit/%s", + "author": {"login": "jdoe"}, + "commit": { + "message": "Add the widget\n\nIt does widget things.\n", + "author": {"name": "Jane Doe", "email": "jane@example.com", "date": "2026-11-01T09:00:00Z"} + } + }`, lookedUpSHA, lookedUpSHA) + })) + defer server.Close() + + commit, err := CommitFinder{Token: "a-token", BaseURL: server.URL + "/"}.CommitBySHA("acme/widget", lookedUpSHA) + require.NoError(t, err) + require.Equal(t, &types.Commit{ + SHA: lookedUpSHA, + Message: "Add the widget\n\nIt does widget things.", + Author: "Jane Doe ", + AuthorUsername: "jdoe", + Timestamp: 1793523600, + URL: "https://github.com/acme/widget/commit/" + lookedUpSHA, + }, commit) + require.Equal(t, "Bearer a-token", gotAuth) + }) + + t.Run("names the repository and commit when GitHub refuses", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = fmt.Fprint(w, `{"message": "Not Found"}`) + })) + defer server.Close() + + _, err := CommitFinder{Token: "a-token", BaseURL: server.URL + "/"}.CommitBySHA("acme/widget", lookedUpSHA) + require.ErrorContains(t, err, "failed to get commit "+lookedUpSHA+" from GitHub repository acme/widget") + require.ErrorContains(t, err, "404 Not Found") + }) + + t.Run("refuses a repository not named owner/name", func(t *testing.T) { + _, err := CommitFinder{Token: "a-token"}.CommitBySHA("widget", lookedUpSHA) + require.EqualError(t, err, `GitHub repository "widget" is not in the form owner/name`) + }) +} diff --git a/internal/intoto/find.go b/internal/intoto/find.go new file mode 100644 index 000000000..fddfb67b0 --- /dev/null +++ b/internal/intoto/find.go @@ -0,0 +1,122 @@ +package intoto + +import ( + "errors" + "fmt" + "os" + "sort" + "strings" +) + +// Source supplies the attestations held about an artifact. Further sources, +// such as GitHub's attestations API or an OCI registry, implement it too. +type Source interface { + // Attestations returns every attestation the source holds that may be + // about the artifact with SHA-256 digest. Find checks each one. + Attestations(digest string) ([]*Attestation, error) + // String names the source in messages. + String() string +} + +// BundleFile is a file holding a Sigstore bundle, a DSSE envelope or an +// in-toto statement, or several one per line. +type BundleFile struct { + Path string +} + +// Attestations reads every attestation in the file; the digest selects +// nothing here. +func (f BundleFile) Attestations(string) ([]*Attestation, error) { + content, err := os.ReadFile(f.Path) + if err != nil { + return nil, err + } + return Read(content) +} + +func (f BundleFile) String() string { + return fmt.Sprintf("bundle file %s", f.Path) +} + +// Found is the attestation chosen to record, with what verifying it +// established. +type Found struct { + *Attestation + Evidence *Evidence + // Rejected holds why other attestations of the same kind about the same + // artifact were not chosen, such as being unsigned, so a caller can report + // them. + Rejected []error +} + +// Find returns the one attestation of kind about the artifact with SHA-256 +// digest that source holds, verified by verifier. When several qualify, a +// verified one is preferred to an unsigned one, then the newest by its +// trusted time, then the one read last. If any signed one fails +// verification, or its signature cannot be checked, Find returns that and +// chooses none: a signature that is bad, or that nobody could check, is a +// stronger signal than a missing one, so an unsigned or verified alternative +// is not recorded in its place. +func Find(source Source, kind PredicateKind, digest string, verifier *Verifier) (*Found, error) { + attestations, err := source.Attestations(digest) + if err != nil { + return nil, fmt.Errorf("%s: %w", source, err) + } + candidates, err := Matching(attestations, kind, digest) + if err != nil { + return nil, fmt.Errorf("%s: %w", source, err) + } + + var accepted []*Found + var rejected, unverifiable []error + for _, candidate := range candidates { + evidence, err := verifier.Verify(candidate, digest) + switch { + case errors.Is(err, ErrFailedVerification), errors.Is(err, ErrCannotVerify): + unverifiable = append(unverifiable, err) + case err != nil: + rejected = append(rejected, err) + default: + accepted = append(accepted, &Found{Attestation: candidate, Evidence: evidence}) + } + } + if len(unverifiable) == 1 { + return nil, fmt.Errorf("%s: %w", source, unverifiable[0]) + } + if len(unverifiable) > 1 { + return nil, fmt.Errorf("%s: %d %s attestations could not be accepted: %w", source, len(unverifiable), kind, joinedErrors(unverifiable)) + } + if len(accepted) == 0 { + if len(rejected) == 1 { + return nil, fmt.Errorf("%s: %w", source, rejected[0]) + } + return nil, fmt.Errorf("%s: none of the %d %s attestations could be accepted: %w", source, len(rejected), kind, joinedErrors(rejected)) + } + + sort.SliceStable(accepted, func(i, j int) bool { + a, b := accepted[i].Evidence, accepted[j].Evidence + if a.Verified != b.Verified { + return !a.Verified + } + return a.Time().Before(b.Time()) + }) + chosen := accepted[len(accepted)-1] + chosen.Rejected = rejected + return chosen, nil +} + +// joinedErrors reports several errors on one line, which reads best on a +// terminal, while errors.Is still finds each of them. +type joinedErrors []error + +func (e joinedErrors) Error() string { + messages := make([]string, len(e)) + for i, err := range e { + messages[i] = err.Error() + } + return strings.Join(messages, "; ") +} + +func (e joinedErrors) Unwrap() []error { + return e +} diff --git a/internal/intoto/fixtures_test.go b/internal/intoto/fixtures_test.go new file mode 100644 index 000000000..26fbf4a4a --- /dev/null +++ b/internal/intoto/fixtures_test.go @@ -0,0 +1,302 @@ +package intoto + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "math/big" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + protodsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" + protorekor "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/testing/ca" + "github.com/sigstore/sigstore-go/pkg/tlog" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + sigdsse "github.com/sigstore/sigstore/pkg/signature/dsse" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" +) + +// regenerateFixturesVar names the variable that rewrites the signed fixtures +// in testdata instead of skipping: +// +// REGENERATE_INTOTO_FIXTURES=1 go test ./internal/intoto -run TestRegenerateFixtures +// +// Every run makes new keys, so the bundles and trusted_root.json must be +// committed together. testdata/README.md describes what each fixture is for. +const regenerateFixturesVar = "REGENERATE_INTOTO_FIXTURES" + +const ( + fixtureIssuer = "https://token.actions.githubusercontent.com" + fixtureIdentity = "https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main" + fixtureOtherIdentity = "https://github.com/mallory/widget/.github/workflows/build.yml@refs/heads/main" + fixtureRunURI = "https://github.com/acme/widget/actions/runs/1234567890/attempts/1" +) + +var ( + oidIssuerV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1} + oidIssuerV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8} + oidSourceRepositoryURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 12} + oidRunInvocationURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 21} +) + +// fixtureSigstore signs like GitHub Actions on Sigstore's public-good +// instance: a Fulcio certificate naming the workflow, a Rekor entry with an +// inclusion proof, and an RFC 3161 timestamp. The Fulcio authority is built +// here rather than taken from ca.VirtualSigstore, whose certificates can only +// name an email address. +type fixtureSigstore struct { + t *testing.T + virtual *ca.VirtualSigstore + fulcio *root.FulcioCertificateAuthority + fulcioKey crypto.Signer + signedAt time.Time +} + +func newFixtureSigstore(t *testing.T) *fixtureSigstore { + virtual, err := ca.NewVirtualSigstore() + require.NoError(t, err) + rootCert, rootKey, err := ca.GenerateRootCa() + require.NoError(t, err) + intermediate, intermediateKey, err := ca.GenerateFulcioIntermediate(rootCert, rootKey) + require.NoError(t, err) + return &fixtureSigstore{ + t: t, + virtual: virtual, + fulcio: &root.FulcioCertificateAuthority{ + Root: rootCert, + Intermediates: []*x509.Certificate{intermediate}, + ValidityPeriodStart: time.Now().Add(-time.Hour), + URI: "https://fulcio.example.test", + }, + fulcioKey: intermediateKey, + signedAt: time.Now(), + } +} + +func (s *fixtureSigstore) trustedRoot() []byte { + trustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, + []root.CertificateAuthority{s.fulcio}, + rawLogIDs(s.t, s.virtual.CTLogs()), + s.virtual.TimestampingAuthorities(), + rawLogIDs(s.t, s.virtual.RekorLogs()), + ) + require.NoError(s.t, err) + content, err := trustedRoot.MarshalJSON() + require.NoError(s.t, err) + return indent(s.t, content) +} + +// rawLogIDs gives each log the raw key ID a trusted root serialises. +// ca.VirtualSigstore holds the hex string's bytes instead, which work only +// in memory: read back from JSON, no log entry would find its log. +func rawLogIDs(t *testing.T, logs map[string]*root.TransparencyLog) map[string]*root.TransparencyLog { + for logID, log := range logs { + id, err := hex.DecodeString(logID) + require.NoError(t, err) + log.ID = id + } + return logs +} + +func (s *fixtureSigstore) leafCertificate(identity string) (*x509.Certificate, *ecdsa.PrivateKey) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(s.t, err) + san, err := url.Parse(identity) + require.NoError(s.t, err) + serial, err := rand.Int(rand.Reader, big.NewInt(1<<62)) + require.NoError(s.t, err) + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{}, + NotBefore: s.signedAt.Add(-time.Minute), + NotAfter: s.signedAt.Add(10 * time.Minute), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + URIs: []*url.URL{san}, + ExtraExtensions: []pkix.Extension{ + {Id: oidIssuerV1, Value: []byte(fixtureIssuer)}, + {Id: oidIssuerV2, Value: utf8String(s.t, fixtureIssuer)}, + {Id: oidSourceRepositoryURI, Value: utf8String(s.t, "https://github.com/acme/widget")}, + {Id: oidRunInvocationURI, Value: utf8String(s.t, fixtureRunURI)}, + }, + } + der, err := x509.CreateCertificate(rand.Reader, template, s.fulcio.Intermediates[0], &key.PublicKey, s.fulcioKey) + require.NoError(s.t, err) + cert, err := x509.ParseCertificate(der) + require.NoError(s.t, err) + return cert, key +} + +// bundle signs statement as identity and returns the Sigstore bundle, logged +// at loggedAfter past the signing time. +func (s *fixtureSigstore) bundle(statement []byte, identity string, loggedAfter time.Duration) []byte { + leaf, key := s.leafCertificate(identity) + envelope := signEnvelope(s.t, key, statement) + sig, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) + require.NoError(s.t, err) + + entry, err := s.virtual.GenerateTlogEntry(leaf, envelope, sig, s.signedAt.Add(loggedAfter).Unix(), true) + require.NoError(s.t, err) + logEntry := entry.TransparencyLogEntry() + var body struct { + Kind string `json:"kind"` + APIVersion string `json:"apiVersion"` + } + require.NoError(s.t, json.Unmarshal(logEntry.CanonicalizedBody, &body)) + logEntry.KindVersion = &protorekor.KindVersion{Kind: body.Kind, Version: body.APIVersion} + // The inclusion proof alone does not vouch for the integrated time; the + // log's signed entry timestamp does, as in a bundle Rekor returns. + logID, err := s.virtual.RekorLogID() + require.NoError(s.t, err) + set, err := s.virtual.RekorSignPayload(tlog.RekorPayload{ + Body: base64.StdEncoding.EncodeToString(logEntry.CanonicalizedBody), + IntegratedTime: logEntry.IntegratedTime, + LogIndex: logEntry.LogIndex, + LogID: logID, + }) + require.NoError(s.t, err) + logEntry.InclusionPromise = &protorekor.InclusionPromise{SignedEntryTimestamp: set} + + timestamp, err := s.virtual.TimestampResponse(sig) + require.NoError(s.t, err) + + b := &protobundle.Bundle{ + MediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", + VerificationMaterial: &protobundle.VerificationMaterial{ + Content: &protobundle.VerificationMaterial_Certificate{Certificate: &protocommon.X509Certificate{RawBytes: leaf.Raw}}, + TlogEntries: []*protorekor.TransparencyLogEntry{logEntry}, + TimestampVerificationData: &protobundle.TimestampVerificationData{ + Rfc3161Timestamps: []*protocommon.RFC3161SignedTimestamp{{SignedTimestamp: timestamp}}, + }, + }, + Content: &protobundle.Bundle_DsseEnvelope{DsseEnvelope: &protodsse.Envelope{ + Payload: statement, + PayloadType: inTotoPayloadType, + Signatures: []*protodsse.Signature{{Sig: sig}}, + }}, + } + content, err := protojson.Marshal(b) + require.NoError(s.t, err) + return content +} + +func signEnvelope(t *testing.T, key *ecdsa.PrivateKey, statement []byte) *dsse.Envelope { + signer, err := signature.LoadECDSASignerVerifier(key, crypto.SHA256) + require.NoError(t, err) + envelopeSigner, err := dsse.NewEnvelopeSigner(&sigdsse.SignerAdapter{SignatureSigner: signer, Pub: &key.PublicKey}) + require.NoError(t, err) + envelope, err := envelopeSigner.SignPayload(context.Background(), inTotoPayloadType, statement) + require.NoError(t, err) + return envelope +} + +// tamper changes the signed statement inside a bundle without re-signing it. +func tamper(t *testing.T, signedBundle []byte, from, to string) []byte { + var b protobundle.Bundle + require.NoError(t, protojson.Unmarshal(signedBundle, &b)) + envelope := b.GetDsseEnvelope() + require.Contains(t, string(envelope.Payload), from) + envelope.Payload = []byte(strings.Replace(string(envelope.Payload), from, to, 1)) + content, err := protojson.Marshal(&b) + require.NoError(t, err) + return content +} + +func utf8String(t *testing.T, s string) []byte { + value, err := asn1.MarshalWithParams(s, "utf8") + require.NoError(t, err) + return value +} + +func indent(t *testing.T, content []byte) []byte { + var value any + require.NoError(t, json.Unmarshal(content, &value)) + out, err := json.MarshalIndent(value, "", " ") + require.NoError(t, err) + return append(out, '\n') +} + +func writeFixture(t *testing.T, name string, content []byte) { + require.NoError(t, os.WriteFile(filepath.Join("testdata", name), content, 0o644)) +} + +func TestRegenerateFixtures(t *testing.T) { + if os.Getenv(regenerateFixturesVar) == "" { + t.Skipf("set %s=1 to rewrite the signed fixtures in testdata", regenerateFixturesVar) + } + s := newFixtureSigstore(t) + statement := func(name string) []byte { return compact(t, statementFixture(t, name)) } + jsonl := func(lines ...[]byte) []byte { + out := []byte{} + for _, line := range lines { + out = append(out, compact(t, line)...) + out = append(out, '\n') + } + return out + } + + writeFixture(t, "trusted_root.json", s.trustedRoot()) + + for _, name := range []string{"github-provenance", "buildkit-provenance", "slsa-v0.2-provenance", "spdx-sbom", "cyclonedx-sbom", "wrong-subject-provenance"} { + writeFixture(t, name+".sigstore.json", indent(t, s.bundle(statement(name+".json"), fixtureIdentity, time.Minute))) + } + writeFixture(t, "wrong-signer-provenance.sigstore.json", + indent(t, s.bundle(statement("github-provenance.json"), fixtureOtherIdentity, time.Minute))) + writeFixture(t, "tampered-provenance.sigstore.json", + indent(t, tamper(t, s.bundle(statement("github-provenance.json"), fixtureIdentity, time.Minute), + "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70", "0000000000000000000000000000000000000000"))) + + writeFixture(t, "attestations.intoto.jsonl", jsonl( + s.bundle(statement("vuln-scan.json"), fixtureIdentity, time.Minute), + s.bundle(statement("github-provenance.json"), fixtureIdentity, time.Minute), + s.bundle(statement("cyclonedx-sbom.json"), fixtureIdentity, time.Minute), + )) + writeFixture(t, "newest-first.intoto.jsonl", jsonl( + s.bundle(statement("github-provenance.json"), fixtureIdentity, 3*time.Minute), + s.bundle(statement("github-provenance.json"), fixtureIdentity, 2*time.Minute), + )) + writeFixture(t, "vuln-only.intoto.jsonl", jsonl( + s.bundle(statement("vuln-scan.json"), fixtureIdentity, time.Minute), + statement("vuln-scan.json"), + )) + writeFixture(t, "wrong-signer-and-unsigned.intoto.jsonl", jsonl( + s.bundle(statement("github-provenance.json"), fixtureOtherIdentity, time.Minute), + statement("github-provenance.json"), + )) + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + publicKey, err := cryptoutils.MarshalPublicKeyToPEM(&key.PublicKey) + require.NoError(t, err) + writeFixture(t, "key.pub", publicKey) + keySigned, err := json.Marshal(signEnvelope(t, key, statement("github-provenance.json"))) + require.NoError(t, err) + writeFixture(t, "key-signed-provenance.dsse.json", indent(t, keySigned)) + + unsigned, err := json.Marshal(dsse.Envelope{ + PayloadType: inTotoPayloadType, + Payload: base64.StdEncoding.EncodeToString(statement("spdx-sbom.json")), + Signatures: []dsse.Signature{}, + }) + require.NoError(t, err) + writeFixture(t, "unsigned-spdx-sbom.dsse.json", indent(t, unsigned)) +} diff --git a/internal/intoto/github-tuf-root.json b/internal/intoto/github-tuf-root.json new file mode 100644 index 000000000..0d20e8f50 --- /dev/null +++ b/internal/intoto/github-tuf-root.json @@ -0,0 +1,167 @@ +{ + "signatures": [ + { + "keyid": "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "sig": "" + }, + { + "keyid": "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "sig": "" + }, + { + "keyid": "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a", + "sig": "" + }, + { + "keyid": "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "sig": "3046022100ca341d3ba2ef7657d69c2825729959681f55aec497b612e81a547e2abb616b49022100cd605b412a3d991f92e0818e07e60383bbd23904723eec221d6e39fdfeae3104" + }, + { + "keyid": "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "sig": "3046022100d0f70effe60d6a18319e2890088cd01d45c654ee6d2ce1d5c3cdcf2dc7f637570221008f947a2d7334d948f1c4794b0a465f1dfb99a578dd8d1f4563cee0581f457db2" + }, + { + "keyid": "54809115b40137aac01af4b7ac2408c77ea0d58fa4dad48fc3196497d2a26f44", + "sig": "304502201ae931db1c48020fb37af54d446ac856306f619dfc3f93ddcff70d2880e443dc022100992b70451aa74805adef24e85ec352e598812267f623979bd4ce719b66b62d22" + }, + { + "keyid": "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "sig": "3045022023bba8e14c177609f43873aa0087ef983ddd2bad9a0a832c0cf279e1be8798f2022100facfaecc1d7ee793042eaaa6970fb9ca700c3bdbf4ee43ed0f8d0fc3aef96563" + }, + { + "keyid": "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "sig": "3046022100d2f6cceb05d135ce6a6ce7fe1dc76c24508154ad71c433028e64ca95ba716ffb022100f0592d60eb67508dd5f9cc593a4cca33bbaf94882c3d74a560fda845a456c6cc" + }, + { + "keyid": "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "sig": "30450221009af2f0c534ed92de909a3b727f7101319c18e10623de8f48a0eba980d3d54d830220095842b16c58567c71f9dfa0b54e79daca1b2fecf3cb2ea4ee6d8393bdf93294" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2025-04-11T14:36:57Z", + "keys": { + "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENki7aZVips5SgRzCd/Om0CGzQKY/\nnv84giqVDmdwb2ys82Z6soFLasvYYEEQcwqaC170n9gr93wHUgPc796uJA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@ashtom" + }, + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElD0o2sOZN9n3RKQ7PtMLAoXj+2Ai\nn4PKT/pfnzDlNLrD3VTQwCc4sR4t+OLu4KQ+qk+kXkR9YuBsu3bdJZ1OWw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@nerdneha" + }, + "54809115b40137aac01af4b7ac2408c77ea0d58fa4dad48fc3196497d2a26f44": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEimKcdST+ORD+g0aGEFDOVZDAaIYg\nIgesNKiIe2L7MUsYx5UHhzQ08quvew13eYSCNJnfwooFZu7cdTu8AwqFjQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@alexiswales" + }, + "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBagkskNOpOTbetTX5CdnvMy+LiWn\nonRrNrqAHL4WgiebH7Uig7GLhC3bkeA/qgb926/vr9qhOPG9Buj2HatrPw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@gregose" + }, + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7IEoVNwrprchXGhT5sAhSax7SOd3\n8duuISghCzfmHdKJWSbV2wJRamRiUVRtmA83K/qm5cT20WXMCT5QeM/D3A==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@trevrosen" + }, + "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC2wJ3xscyXxBLybJ9FVjwkyQMe53\nRHUz77AjMO8MzVaT8xw6ZvJqdNZiytYtigWULlINxw6frNsWJKb/f7lC8A==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@kommendorkapten" + }, + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDdORwcruW3gqAgaLjH/nNdGMB4kQ\nAvA+wD6DyO4P/wR8ee2ce83NZHq1ZADKhve0rlYKaKy3CqyQ5SmlZ36Zhw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@krukow" + }, + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENynVdQnM9h7xU71G7PiJpQaDemub\nkbjsjYwLlPJTQVuxQO8WeIpJf8MEh5rf01t2dDIuCsZ5gRx+QvDv0UzfsA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@mph4" + }, + "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENKNcNcX+d73lS1TRFb9Vnp8JvOoh\nzYQ+in43iGenbG8RGo9L/6FJ2hoRbVU6xskvyuErcdPbCdI4GxrQ5i8hkw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "azurekms://production-tuf-root.vault.azure.net/keys/Online-Key/aaf375fd8ed24acb949a5cc173700b05" + } + }, + "roles": { + "root": { + "keyids": [ + "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a", + "54809115b40137aac01af4b7ac2408c77ea0d58fa4dad48fc3196497d2a26f44" + ], + "threshold": 3 + }, + "snapshot": { + "keyids": [ + "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 21, + "x-tuf-on-ci-signing-period": 7 + }, + "targets": { + "keyids": [ + "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a", + "54809115b40137aac01af4b7ac2408c77ea0d58fa4dad48fc3196497d2a26f44" + ], + "threshold": 3 + }, + "timestamp": { + "keyids": [ + "eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 7, + "x-tuf-on-ci-signing-period": 6 + } + }, + "spec_version": "1.0.31", + "version": 3, + "x-tuf-on-ci-expiry-period": 240, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/intoto/match.go b/internal/intoto/match.go new file mode 100644 index 000000000..9a7924d59 --- /dev/null +++ b/internal/intoto/match.go @@ -0,0 +1,142 @@ +package intoto + +import ( + "fmt" + "slices" + "strings" +) + +// PredicateKind is a family of predicate types recorded the same way in Kosli. +type PredicateKind string + +const ( + // KindProvenance matches SLSA provenance v1 and v0.2. + KindProvenance PredicateKind = "provenance" + // KindSBOM matches SPDX and CycloneDX documents. + KindSBOM PredicateKind = "sbom" +) + +const ( + slsaProvenanceV1 = "https://slsa.dev/provenance/v1" + slsaProvenanceV02 = "https://slsa.dev/provenance/v0.2" + spdxDocument = "https://spdx.dev/Document" + cycloneDXBOM = "https://cyclonedx.org/bom" +) + +// PredicateKinds lists every kind, in the order a message offers them. +var PredicateKinds = []PredicateKind{KindProvenance, KindSBOM} + +// ParsePredicateKind returns the kind named by s. +func ParsePredicateKind(s string) (PredicateKind, error) { + for _, kind := range PredicateKinds { + if string(kind) == s { + return kind, nil + } + } + return "", fmt.Errorf("predicate type %q is not supported. Must be one of: %s", s, kindList()) +} + +func kindList() string { + names := make([]string, len(PredicateKinds)) + for i, kind := range PredicateKinds { + names[i] = string(kind) + } + return strings.Join(names, ", ") +} + +// Matches reports whether predicateType is one of this kind's predicate types. +// SPDX and CycloneDX producers may append a version to the type URI. +func (k PredicateKind) Matches(predicateType string) bool { + switch k { + case KindProvenance: + return predicateType == slsaProvenanceV1 || predicateType == slsaProvenanceV02 + case KindSBOM: + return hasVersionedPrefix(predicateType, spdxDocument) || hasVersionedPrefix(predicateType, cycloneDXBOM) + } + return false +} + +func hasVersionedPrefix(predicateType, base string) bool { + return predicateType == base || strings.HasPrefix(predicateType, base+"/") +} + +func (k PredicateKind) description() string { + switch k { + case KindProvenance: + return "SLSA provenance v1 or v0.2" + case KindSBOM: + return "SPDX or CycloneDX SBOM" + } + return string(k) +} + +// Matching returns the attestations of the given kind that list the artifact +// with SHA-256 digest as a subject. It fails, saying what it found instead, +// when there are none. +func Matching(attestations []*Attestation, kind PredicateKind, digest string) ([]*Attestation, error) { + digest = normaliseDigest(digest) + + var ofKind []*Attestation + var otherTypes []string + for _, a := range attestations { + if kind.Matches(a.Statement.GetPredicateType()) { + ofKind = append(ofKind, a) + } else if !slices.Contains(otherTypes, a.Statement.GetPredicateType()) { + otherTypes = append(otherTypes, a.Statement.GetPredicateType()) + } + } + if len(ofKind) == 0 { + return nil, fmt.Errorf("found no %s attestation (%s); the predicate types found were: %s", + kind, kind.description(), strings.Join(otherTypes, ", ")) + } + + var matching []*Attestation + var subjects []string + for _, a := range ofKind { + if a.HasSubject(digest) { + matching = append(matching, a) + continue + } + for _, subject := range a.Statement.GetSubject() { + subjects = append(subjects, describeSubject(subject.GetName(), subject.GetDigest()["sha256"])) + } + } + if len(matching) == 0 { + return nil, fmt.Errorf("no %s attestation lists the artifact (sha256:%s) as its subject; the subjects found were: %s", + kind, digest, strings.Join(subjects, ", ")) + } + return matching, nil +} + +// HasSubject reports whether the statement lists the artifact with SHA-256 +// digest among its subjects. +func (a *Attestation) HasSubject(digest string) bool { + digest = normaliseDigest(digest) + for _, subject := range a.Statement.GetSubject() { + if strings.EqualFold(subject.GetDigest()["sha256"], digest) { + return true + } + } + return false +} + +// RequireSubject is HasSubject as an error that names the artifact, for a +// statement that need not be signed to say which artifact it is about. +func (a *Attestation) RequireSubject(digest string) error { + if !a.HasSubject(digest) { + return fmt.Errorf("%s does not list the artifact (sha256:%s) as its subject", a.Describe(), normaliseDigest(digest)) + } + return nil +} + +func describeSubject(name, sha256 string) string { + if sha256 == "" { + return fmt.Sprintf("%s (no sha256 digest)", name) + } + return fmt.Sprintf("%s (sha256:%s)", name, sha256) +} + +// normaliseDigest accepts a digest with or without its algorithm prefix. +func normaliseDigest(digest string) string { + return strings.ToLower(strings.TrimPrefix(digest, "sha256:")) +} diff --git a/internal/intoto/match_test.go b/internal/intoto/match_test.go new file mode 100644 index 000000000..8d4004757 --- /dev/null +++ b/internal/intoto/match_test.go @@ -0,0 +1,90 @@ +package intoto + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func readFixtures(t *testing.T, names ...string) []*Attestation { + t.Helper() + lines := make([][]byte, len(names)) + for i, name := range names { + lines[i] = compact(t, statementFixture(t, name)) + } + attestations, err := Read(bytes.Join(lines, []byte("\n"))) + require.NoError(t, err) + return attestations +} + +func TestPredicateKind(t *testing.T) { + t.Run("provenance matches SLSA v1 and v0.2 only", func(t *testing.T) { + require.True(t, KindProvenance.Matches("https://slsa.dev/provenance/v1")) + require.True(t, KindProvenance.Matches("https://slsa.dev/provenance/v0.2")) + require.False(t, KindProvenance.Matches("https://slsa.dev/provenance/v0.1")) + require.False(t, KindProvenance.Matches("https://spdx.dev/Document")) + }) + + t.Run("sbom matches SPDX and CycloneDX, versioned or not", func(t *testing.T) { + require.True(t, KindSBOM.Matches("https://spdx.dev/Document")) + require.True(t, KindSBOM.Matches("https://spdx.dev/Document/v2.3")) + require.True(t, KindSBOM.Matches("https://cyclonedx.org/bom")) + require.True(t, KindSBOM.Matches("https://cyclonedx.org/bom/v1.6")) + require.False(t, KindSBOM.Matches("https://spdx.dev/Documentation")) + require.False(t, KindSBOM.Matches("https://slsa.dev/provenance/v1")) + }) + + t.Run("parses the names --predicate-type accepts", func(t *testing.T) { + kind, err := ParsePredicateKind("sbom") + require.NoError(t, err) + require.Equal(t, KindSBOM, kind) + + _, err = ParsePredicateKind("vuln") + require.EqualError(t, err, `predicate type "vuln" is not supported. Must be one of: provenance, sbom`) + }) +} + +func TestMatching(t *testing.T) { + t.Run("keeps the attestations of the kind asked for", func(t *testing.T) { + attestations := readFixtures(t, "vuln-scan.json", "github-provenance.json", "cyclonedx-sbom.json", "spdx-sbom.json") + + matching, err := Matching(attestations, KindSBOM, artifactDigest) + require.NoError(t, err) + require.Len(t, matching, 2) + require.Equal(t, 3, matching[0].Position) + require.Equal(t, 4, matching[1].Position) + }) + + t.Run("accepts a digest with its algorithm prefix, in any case", func(t *testing.T) { + attestations := readFixtures(t, "github-provenance.json") + + matching, err := Matching(attestations, KindProvenance, "sha256:"+strings.ToUpper(artifactDigest)) + require.NoError(t, err) + require.Len(t, matching, 1) + }) + + t.Run("names the predicate types found when none is of the kind asked for", func(t *testing.T) { + attestations := readFixtures(t, "vuln-scan.json", "vuln-scan.json", "spdx-sbom.json") + + _, err := Matching(attestations, KindProvenance, artifactDigest) + require.EqualError(t, err, "found no provenance attestation (SLSA provenance v1 or v0.2); the predicate types found were: https://cosign.sigstore.dev/attestation/vuln/v1, https://spdx.dev/Document/v2.3") + }) + + t.Run("names the subjects found when none is the artifact", func(t *testing.T) { + attestations := readFixtures(t, "wrong-subject-provenance.json") + + _, err := Matching(attestations, KindProvenance, artifactDigest) + require.EqualError(t, err, "no provenance attestation lists the artifact (sha256:"+artifactDigest+") as its subject; the subjects found were: other.bin (sha256:11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8)") + }) + + t.Run("skips an attestation about another artifact when one is about this one", func(t *testing.T) { + attestations := readFixtures(t, "wrong-subject-provenance.json", "github-provenance.json") + + matching, err := Matching(attestations, KindProvenance, artifactDigest) + require.NoError(t, err) + require.Len(t, matching, 1) + require.Equal(t, 2, matching[0].Position) + }) +} diff --git a/internal/intoto/provenance.go b/internal/intoto/provenance.go new file mode 100644 index 000000000..0bc5374b8 --- /dev/null +++ b/internal/intoto/provenance.go @@ -0,0 +1,330 @@ +package intoto + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +const ( + githubActionsBuildType = "https://actions.github.io/buildtypes/workflow/v1" + slsaGitHubBuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1" + slsaGitHubGeneratorV02 = "https://github.com/slsa-framework/slsa-github-generator/" + buildKitBuildTypeV1 = "https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md" + buildKitBuildTypeV02 = "https://mobyproject.org/buildkit@v1" + buildKitMetadataV02 = "https://mobyproject.org/buildkit@v1#metadata" +) + +// Provenance is what a provenance statement says about the build that made an +// artifact, in the terms Kosli records an artifact by. It is filled from the +// statement alone, never from the environment reading it, because that may be +// a different job or workflow from the one that ran the build. +type Provenance struct { + // Filename is the subject's name for the artifact. + Filename string + BuilderID string + BuildType string + // BuildURL is the run that built the artifact. + BuildURL string + // Commit is the SHA of the git commit the artifact was built from. + Commit string + Branch string + Repository Repository + CommitURL string + // Workflow is the CI workflow file that ran the build, when the build + // type names one. + Workflow string + // Missing names what a caller would want but the statement did not say, + // as Missing* values. + Missing []string +} + +// The values of Provenance.Missing. +const ( + MissingBuildURL = "build URL" + MissingCommit = "commit" + MissingCommitURL = "commit URL" + MissingRepository = "repository" +) + +// Repository is the source repository a provenance statement says was built. +type Repository struct { + // URL is the repository's web address, such as https://github.com/acme/widget. + URL string + // Name is the repository's path on its host, such as acme/widget. + Name string + // ID is the host's identifier for the repository, when the statement + // carries one. + ID string + Host string + // Provider is the Kosli repo provider for the host: github, gitlab or + // bitbucket_cloud, or empty when the host is none of those. + Provider string +} + +// ProvenanceFrom maps a provenance attestation about the artifact with +// SHA-256 digest. repoProvider names the provider for a repository host it +// does not recognise, such as a self-hosted GitHub Enterprise. +func ProvenanceFrom(a *Attestation, digest, repoProvider string) (*Provenance, error) { + if !KindProvenance.Matches(a.Statement.GetPredicateType()) { + return nil, fmt.Errorf("%s is %s, not SLSA provenance", a.Describe(), a.Statement.GetPredicateType()) + } + var predicate map[string]any + if err := json.Unmarshal(a.Predicate, &predicate); err != nil { + return nil, fmt.Errorf("%s: the provenance predicate is not a JSON object: %w", a.Describe(), err) + } + + p := &Provenance{Filename: subjectName(a, digest)} + if a.Statement.GetPredicateType() == slsaProvenanceV1 { + p.fromV1(predicate) + } else { + p.fromV02(predicate) + } + p.finish(repoProvider) + return p, nil +} + +func subjectName(a *Attestation, digest string) string { + digest = normaliseDigest(digest) + for _, subject := range a.Statement.GetSubject() { + if strings.EqualFold(subject.GetDigest()["sha256"], digest) { + return subject.GetName() + } + } + return "" +} + +func (p *Provenance) fromV1(predicate map[string]any) { + p.BuildType = str(predicate, "buildDefinition", "buildType") + p.BuilderID = str(predicate, "runDetails", "builder", "id") + // BuildKit writes invocationID, against the SLSA spec's invocationId. + invocation := firstOf(str(predicate, "runDetails", "metadata", "invocationId"), str(predicate, "runDetails", "metadata", "invocationID")) + dependencies := list(predicate, "buildDefinition", "resolvedDependencies") + + switch p.BuildType { + case githubActionsBuildType, slsaGitHubBuildType: + workflow := obj(predicate, "buildDefinition", "externalParameters", "workflow") + p.BuildURL = webURL(invocation) + p.Workflow = str(workflow, "path") + p.Repository.URL = str(workflow, "repository") + p.Repository.ID = str(predicate, "buildDefinition", "internalParameters", "github", "repository_id") + p.Branch = branchOf(str(workflow, "ref")) + p.Commit = sourceCommit(dependencies, p.Repository.URL) + case buildKitBuildTypeV1: + p.fromBuildKit(obj(predicate, "runDetails", "metadata", "buildkit_metadata", "vcs"), + obj(predicate, "buildDefinition", "externalParameters", "configSource"), invocation, dependencies) + default: + p.BuildURL = webURL(invocation) + p.Commit = sourceCommit(dependencies, "") + } + p.fillFromDependency(dependencies) +} + +func (p *Provenance) fromV02(predicate map[string]any) { + p.BuildType = str(predicate, "buildType") + p.BuilderID = str(predicate, "builder", "id") + // BuildKit writes buildInvocationID, against the SLSA spec's buildInvocationId. + invocation := firstOf(str(predicate, "metadata", "buildInvocationId"), str(predicate, "metadata", "buildInvocationID")) + configSource := obj(predicate, "invocation", "configSource") + materials := list(predicate, "materials") + + switch { + case strings.HasPrefix(p.BuildType, slsaGitHubGeneratorV02): + p.BuildURL = webURL(invocation) + p.Workflow = str(configSource, "entryPoint") + p.Repository.ID = str(predicate, "invocation", "environment", "github_repository_id") + p.Repository.URL, p.Branch = repoAndBranch(str(configSource, "uri")) + p.Commit = firstOf(str(configSource, "digest", "sha1"), sourceCommit(materials, p.Repository.URL)) + case p.BuildType == buildKitBuildTypeV02: + p.fromBuildKit(obj(predicate, "metadata", buildKitMetadataV02, "vcs"), configSource, invocation, materials) + default: + p.BuildURL = webURL(invocation) + p.Commit = firstOf(str(configSource, "digest", "sha1"), sourceCommit(materials, "")) + p.Repository.URL, p.Branch = repoAndBranch(str(configSource, "uri")) + } + p.fillFromDependency(materials) +} + +// fromBuildKit reads BuildKit's provenance. Its invocation ID is a build +// reference, not a URL; docker/build-push-action puts the run's URL in the +// builder ID instead. +func (p *Provenance) fromBuildKit(vcs, configSource map[string]any, invocation string, dependencies []any) { + p.BuildURL = firstOf(webURL(p.BuilderID), webURL(invocation)) + p.Repository.URL = str(vcs, "source") + repo, branch := repoAndBranch(str(configSource, "uri")) + p.Repository.URL = firstOf(p.Repository.URL, repo) + p.Branch = branch + p.Commit = firstOf(str(vcs, "revision"), str(configSource, "digest", "sha1"), sourceCommit(dependencies, p.Repository.URL)) +} + +// fillFromDependency takes the repository and branch from the git +// dependency that supplied the commit, when the build type did not say. +func (p *Provenance) fillFromDependency(dependencies []any) { + for _, d := range dependencies { + dependency, _ := d.(map[string]any) + if p.Commit == "" || dependencyCommit(dependency) != p.Commit { + continue + } + repo, branch := repoAndBranch(str(dependency, "uri")) + p.Repository.URL = firstOf(p.Repository.URL, repo) + p.Branch = firstOf(p.Branch, branch) + return + } +} + +func (p *Provenance) finish(repoProvider string) { + if repo, err := url.Parse(p.Repository.URL); err == nil && repo.Host != "" { + p.Repository.URL = strings.TrimSuffix(strings.TrimSuffix(p.Repository.URL, "/"), ".git") + p.Repository.Host = repo.Host + p.Repository.Name = strings.TrimSuffix(strings.Trim(repo.Path, "/"), ".git") + p.Repository.Provider = providerFor(repo.Host, repoProvider) + } else { + p.Repository = Repository{} + } + if p.Commit != "" && p.Repository.URL != "" { + p.CommitURL = commitURL(p.Repository.Provider, p.Repository.URL, p.Commit) + } + + for _, field := range []struct{ name, value string }{ + {MissingBuildURL, p.BuildURL}, + {MissingCommit, p.Commit}, + {MissingCommitURL, p.CommitURL}, + {MissingRepository, p.Repository.URL}, + } { + if field.value == "" { + p.Missing = append(p.Missing, field.name) + } + } +} + +func providerFor(host, fallback string) string { + switch strings.ToLower(host) { + case "github.com": + return "github" + case "gitlab.com": + return "gitlab" + case "bitbucket.org": + return "bitbucket_cloud" + } + return fallback +} + +func commitURL(provider, repoURL, commit string) string { + switch provider { + case "github": + return repoURL + "/commit/" + commit + case "gitlab": + return repoURL + "/-/commit/" + commit + case "bitbucket", "bitbucket_cloud": + return repoURL + "/commits/" + commit + } + return "" +} + +// sourceCommit returns the commit of the first git dependency, preferring +// one from repoURL when that is known. +func sourceCommit(dependencies []any, repoURL string) string { + fallback := "" + for _, d := range dependencies { + dependency, _ := d.(map[string]any) + commit := dependencyCommit(dependency) + if commit == "" { + continue + } + repo, _ := repoAndBranch(str(dependency, "uri")) + if repoURL == "" || sameRepo(repo, repoURL) { + return commit + } + fallback = firstOf(fallback, commit) + } + return fallback +} + +func dependencyCommit(dependency map[string]any) string { + commit := firstOf(str(dependency, "digest", "gitCommit"), str(dependency, "digest", "sha1")) + if commit == "" || !strings.HasPrefix(str(dependency, "uri"), "git+") && !strings.Contains(str(dependency, "uri"), ".git") { + return "" + } + return commit +} + +func sameRepo(a, b string) bool { + normalise := func(s string) string { + return strings.ToLower(strings.TrimSuffix(strings.TrimSuffix(s, "/"), ".git")) + } + return normalise(a) == normalise(b) +} + +// repoAndBranch splits a source URI, in the forms +// git+https://host/owner/repo@refs/heads/main and +// https://host/owner/repo.git#refs/heads/main, into the repository's web +// address and the branch. The branch is empty for a tag or any other ref. +func repoAndBranch(uri string) (string, string) { + uri = strings.TrimPrefix(uri, "git+") + repo, ref, found := strings.Cut(uri, "#") + if !found { + if parsed, err := url.Parse(uri); err == nil && parsed.Host != "" { + if path, pathRef, hasRef := strings.Cut(parsed.Path, "@"); hasRef { + repo, ref = parsed.Scheme+"://"+parsed.Host+path, pathRef + } + } + } + if webURL(repo) == "" { + return "", "" + } + return strings.TrimSuffix(repo, ".git"), branchOf(ref) +} + +func branchOf(ref string) string { + if branch, ok := strings.CutPrefix(ref, "refs/heads/"); ok { + return branch + } + return "" +} + +// webURL returns s when it is an http or https URL, and "" otherwise. +func webURL(s string) string { + parsed, err := url.Parse(s) + if err != nil || parsed.Host == "" || (parsed.Scheme != "https" && parsed.Scheme != "http") { + return "" + } + return s +} + +func firstOf(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} + +func obj(m map[string]any, path ...string) map[string]any { + current := m + for _, key := range path { + next, ok := current[key].(map[string]any) + if !ok { + return nil + } + current = next + } + return current +} + +func str(m map[string]any, path ...string) string { + if len(path) == 0 { + return "" + } + value, _ := obj(m, path[:len(path)-1]...)[path[len(path)-1]].(string) + return value +} + +func list(m map[string]any, path ...string) []any { + if len(path) == 0 { + return nil + } + value, _ := obj(m, path[:len(path)-1]...)[path[len(path)-1]].([]any) + return value +} diff --git a/internal/intoto/provenance_test.go b/internal/intoto/provenance_test.go new file mode 100644 index 000000000..40c78936b --- /dev/null +++ b/internal/intoto/provenance_test.go @@ -0,0 +1,206 @@ +package intoto + +import ( + "strings" + "testing" + + "github.com/kosli-dev/cli/internal/sbom" + "github.com/stretchr/testify/require" +) + +const fixtureCommit = "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70" + +// statementWith reads a statement fixture with each old string replaced by +// its new one, for variations on a fixture that need no file of their own. +func statementWith(t *testing.T, name string, replacements ...string) *Attestation { + t.Helper() + content := string(statementFixture(t, name)) + for i := 0; i < len(replacements); i += 2 { + require.Contains(t, content, replacements[i]) + content = strings.ReplaceAll(content, replacements[i], replacements[i+1]) + } + attestations, err := Read([]byte(content)) + require.NoError(t, err) + return attestations[0] +} + +func TestProvenanceFromGitHubActions(t *testing.T) { + p, err := ProvenanceFrom(statementWith(t, "github-provenance.json"), artifactDigest, "") + require.NoError(t, err) + require.Equal(t, &Provenance{ + Filename: "artifact.bin", + BuilderID: "https://github.com/actions/runner/github-hosted", + BuildType: "https://actions.github.io/buildtypes/workflow/v1", + BuildURL: "https://github.com/acme/widget/actions/runs/1234567890/attempts/1", + Commit: fixtureCommit, + Branch: "main", + Repository: Repository{ + URL: "https://github.com/acme/widget", + Name: "acme/widget", + ID: "123456789", + Host: "github.com", + Provider: "github", + }, + CommitURL: "https://github.com/acme/widget/commit/" + fixtureCommit, + Workflow: ".github/workflows/build.yml", + }, p) +} + +func TestProvenanceFromGitHubActionsWithoutARunURL(t *testing.T) { + attestation := statementWith(t, "github-provenance.json", + `"invocationId": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1"`, `"invocationId": "local-17"`) + + p, err := ProvenanceFrom(attestation, artifactDigest, "") + require.NoError(t, err) + require.Empty(t, p.BuildURL, "an invocation ID that is not a URL is not a build URL") + require.Equal(t, []string{"build URL"}, p.Missing) +} + +func TestProvenanceFromBuildKit(t *testing.T) { + t.Run("reads the commit and repository from BuildKit's VCS metadata, and the run from the builder ID", func(t *testing.T) { + p, err := ProvenanceFrom(statementWith(t, "buildkit-provenance.json"), artifactDigest, "") + require.NoError(t, err) + require.Equal(t, &Provenance{ + Filename: "pkg:docker/acme/widget@1.2.3?platform=linux%2Famd64", + BuilderID: "https://github.com/acme/widget/actions/runs/1234567890/attempts/1", + BuildType: "https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md", + BuildURL: "https://github.com/acme/widget/actions/runs/1234567890/attempts/1", + Commit: fixtureCommit, + Branch: "main", + Repository: Repository{ + URL: "https://github.com/acme/widget", + Name: "acme/widget", + Host: "github.com", + Provider: "github", + }, + CommitURL: "https://github.com/acme/widget/commit/" + fixtureCommit, + }, p) + }) + + t.Run("has no build URL when built outside CI, since its invocation ID is not one", func(t *testing.T) { + p, err := ProvenanceFrom(statementWith(t, "buildkit-provenance.json", + `"id": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1"`, `"id": ""`), artifactDigest, "") + require.NoError(t, err) + require.Empty(t, p.BuildURL) + require.Equal(t, []string{"build URL"}, p.Missing) + }) + + t.Run("falls back to the build's config source when there is no VCS metadata", func(t *testing.T) { + p, err := ProvenanceFrom(statementWith(t, "buildkit-provenance.json", + `"source": "https://github.com/acme/widget",`, ``, + `"revision": "`+fixtureCommit+`"`, `"revision": ""`), artifactDigest, "") + require.NoError(t, err) + require.Equal(t, fixtureCommit, p.Commit) + require.Equal(t, "https://github.com/acme/widget", p.Repository.URL) + require.Equal(t, "main", p.Branch) + }) +} + +func TestProvenanceFromSLSAv02(t *testing.T) { + attestations, err := Read(statementFixture(t, "slsa-v0.2-provenance.json")) + require.NoError(t, err) + + p, err := ProvenanceFrom(attestations[0], artifactDigest, "") + require.NoError(t, err) + require.Equal(t, &Provenance{ + Filename: "artifact.bin", + BuilderID: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0", + BuildType: "https://github.com/slsa-framework/slsa-github-generator/generic@v1", + BuildURL: "https://github.com/acme/widget/actions/runs/1234567890/attempts/1", + Commit: fixtureCommit, + Branch: "main", + Repository: Repository{ + URL: "https://github.com/acme/widget", + Name: "acme/widget", + ID: "123456789", + Host: "github.com", + Provider: "github", + }, + CommitURL: "https://github.com/acme/widget/commit/" + fixtureCommit, + Workflow: ".github/workflows/release.yml", + }, p) +} + +func TestProvenanceFromOtherSources(t *testing.T) { + t.Run("uses the provider given for a host it does not recognise", func(t *testing.T) { + attestation := statementWith(t, "github-provenance.json", "https://github.com/acme/widget", "https://github.example.com/acme/widget") + + p, err := ProvenanceFrom(attestation, artifactDigest, "github") + require.NoError(t, err) + require.Equal(t, "github.example.com", p.Repository.Host) + require.Equal(t, "github", p.Repository.Provider) + require.Equal(t, "https://github.example.com/acme/widget/commit/"+fixtureCommit, p.CommitURL) + }) + + t.Run("cannot build a commit URL for an unknown host with no provider", func(t *testing.T) { + attestation := statementWith(t, "github-provenance.json", "https://github.com/acme/widget", "https://git.example.com/acme/widget") + + p, err := ProvenanceFrom(attestation, artifactDigest, "") + require.NoError(t, err) + require.Empty(t, p.Repository.Provider) + require.Empty(t, p.CommitURL) + require.Equal(t, []string{"commit URL"}, p.Missing) + }) + + t.Run("derives GitLab commit URLs", func(t *testing.T) { + attestation := statementWith(t, "github-provenance.json", "https://github.com/acme/widget", "https://gitlab.com/acme/widget") + + p, err := ProvenanceFrom(attestation, artifactDigest, "") + require.NoError(t, err) + require.Equal(t, "gitlab", p.Repository.Provider) + require.Equal(t, "https://gitlab.com/acme/widget/-/commit/"+fixtureCommit, p.CommitURL) + }) + + t.Run("records what an unknown build type offers and says what is missing", func(t *testing.T) { + attestation := statementWith(t, "github-provenance.json", + "https://actions.github.io/buildtypes/workflow/v1", "https://example.com/build/v1", + `"invocationId": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1"`, `"invocationId": "local-build-17"`) + + p, err := ProvenanceFrom(attestation, artifactDigest, "") + require.NoError(t, err) + require.Equal(t, "https://example.com/build/v1", p.BuildType) + require.Equal(t, fixtureCommit, p.Commit) + require.Equal(t, "https://github.com/acme/widget", p.Repository.URL) + require.Equal(t, "main", p.Branch) + require.Empty(t, p.Workflow) + require.Equal(t, []string{"build URL"}, p.Missing) + }) + + t.Run("says everything is missing from an empty predicate", func(t *testing.T) { + attestations, err := Read([]byte(`{"_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "a", "digest": {"sha256": "` + artifactDigest + `"}}], "predicateType": "https://slsa.dev/provenance/v1", "predicate": {}}`)) + require.NoError(t, err) + + p, err := ProvenanceFrom(attestations[0], artifactDigest, "") + require.NoError(t, err) + require.Equal(t, []string{"build URL", "commit", "commit URL", "repository"}, p.Missing) + }) + + t.Run("refuses an attestation that is not provenance", func(t *testing.T) { + _, err := ProvenanceFrom(statementWith(t, "spdx-sbom.json"), artifactDigest, "") + require.EqualError(t, err, "the attestation is https://spdx.dev/Document/v2.3, not SLSA provenance") + }) +} + +func TestSBOMDocument(t *testing.T) { + t.Run("hands internal/sbom the SPDX document", func(t *testing.T) { + document, err := SBOMDocument(statementWith(t, "spdx-sbom.json")) + require.NoError(t, err) + data, err := sbom.ProcessSBOM(document) + require.NoError(t, err) + require.Equal(t, "spdx-2.3", data.Format) + require.Equal(t, 1, data.Document.PackageCount) + }) + + t.Run("hands internal/sbom the CycloneDX document", func(t *testing.T) { + document, err := SBOMDocument(statementWith(t, "cyclonedx-sbom.json")) + require.NoError(t, err) + data, err := sbom.ProcessSBOM(document) + require.NoError(t, err) + require.Equal(t, "cyclonedx-1.6", data.Format) + }) + + t.Run("refuses an attestation that is not an SBOM", func(t *testing.T) { + _, err := SBOMDocument(statementWith(t, "github-provenance.json")) + require.EqualError(t, err, "the attestation is https://slsa.dev/provenance/v1, not an SPDX or CycloneDX SBOM") + }) +} diff --git a/internal/intoto/read.go b/internal/intoto/read.go new file mode 100644 index 000000000..34d455ebf --- /dev/null +++ b/internal/intoto/read.go @@ -0,0 +1,207 @@ +// Package intoto reads in-toto attestations, verifies the signed ones against a +// pinned signer, and maps their predicates onto the records Kosli keeps. +// +// The command layer only chooses a source and builds a request from what this +// package returns, so other callers can use it directly. +package intoto + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + ita1 "github.com/in-toto/attestation/go/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore-go/pkg/bundle" + "google.golang.org/protobuf/encoding/protojson" +) + +const ( + inTotoPayloadType = "application/vnd.in-toto+json" + bundleMediaTypePrefix = "application/vnd.dev.sigstore.bundle" + statementTypePrefix = "https://in-toto.io/Statement/" +) + +// Attestation is one in-toto statement read from a source, together with the +// signature over it, if it has one. +type Attestation struct { + // Raw is the bytes the attestation was read from: the whole file, or one + // line of a file holding several. + Raw []byte + // Position is the attestation's 1-based place in a file holding several, + // and 0 in a file holding one. + Position int + Statement *ita1.Statement + // Predicate is the predicate exactly as the payload carried it. The + // Statement's copy has been through protobuf's Struct, which turns every + // number into a float64. + Predicate json.RawMessage + + bundle *bundle.Bundle + envelope *dsse.Envelope +} + +// Signed reports whether the attestation carries a signature to verify. +func (a *Attestation) Signed() bool { + if a.bundle != nil { + return true + } + return a.envelope != nil && len(a.envelope.Signatures) > 0 +} + +// Describe names the attestation in a message: by its place in the file when +// the file holds several. +func (a *Attestation) Describe() string { + if a.Position == 0 { + return "the attestation" + } + return fmt.Sprintf("attestation %d", a.Position) +} + +// Read parses content holding one attestation, or several one after another +// as in a .intoto.jsonl file. Each is a Sigstore bundle, a DSSE envelope, or a +// bare in-toto statement. Nothing is verified here. +func Read(content []byte) ([]*Attestation, error) { + documents, err := splitDocuments(content) + if err != nil { + return nil, err + } + + attestations := make([]*Attestation, 0, len(documents)) + for i, document := range documents { + attestation := &Attestation{Raw: document} + if len(documents) == 1 { + // The whole file, so a checksum of Raw is a checksum of the file. + attestation.Raw = content + } else { + attestation.Position = i + 1 + } + if err := attestation.parse(document); err != nil { + return nil, fmt.Errorf("%s: %w", attestation.Describe(), err) + } + attestations = append(attestations, attestation) + } + return attestations, nil +} + +// ErrSeveralDocuments is ReadEnvelope's error for content holding more than +// one JSON document, such as an .intoto.jsonl file, which is neither a single +// bundle or envelope nor a document of its own. +var ErrSeveralDocuments = errors.New("holds several JSON documents, not a single bundle or envelope") + +// ReadEnvelope reads content as a single Sigstore bundle or DSSE envelope. +// wrapped is false, with no error, for anything else, so a caller can treat +// that content as the document itself; a malformed bundle or envelope is +// still an error, and so is ErrSeveralDocuments. +func ReadEnvelope(content []byte) (attestation *Attestation, wrapped bool, err error) { + var probe struct { + MediaType string `json:"mediaType"` + PayloadType string `json:"payloadType"` + } + if json.Unmarshal(content, &probe) != nil { + if documents, err := splitDocuments(content); err == nil && len(documents) > 1 { + return nil, false, ErrSeveralDocuments + } + return nil, false, nil + } + if !strings.HasPrefix(probe.MediaType, bundleMediaTypePrefix) && probe.PayloadType == "" { + return nil, false, nil + } + attestations, err := Read(content) + if err != nil { + return nil, true, err + } + return attestations[0], true, nil +} + +func splitDocuments(content []byte) ([]json.RawMessage, error) { + decoder := json.NewDecoder(bytes.NewReader(content)) + var documents []json.RawMessage + for { + var document json.RawMessage + err := decoder.Decode(&document) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("not valid JSON: %w", err) + } + documents = append(documents, document) + } + if len(documents) == 0 { + return nil, fmt.Errorf("holds no attestation") + } + return documents, nil +} + +func (a *Attestation) parse(document []byte) error { + var probe struct { + MediaType string `json:"mediaType"` + PayloadType string `json:"payloadType"` + Type string `json:"_type"` + } + if err := json.Unmarshal(document, &probe); err != nil { + return fmt.Errorf("expected a JSON object: %w", err) + } + + switch { + case strings.HasPrefix(probe.MediaType, bundleMediaTypePrefix): + return a.parseBundle(document) + case probe.PayloadType != "": + var envelope dsse.Envelope + if err := json.Unmarshal(document, &envelope); err != nil { + return fmt.Errorf("not a valid DSSE envelope: %w", err) + } + return a.parseEnvelope(&envelope) + case strings.HasPrefix(probe.Type, statementTypePrefix): + return a.parseStatement(document) + } + return fmt.Errorf("not a Sigstore bundle, a DSSE envelope or an in-toto statement") +} + +func (a *Attestation) parseBundle(document []byte) error { + var b bundle.Bundle + if err := b.UnmarshalJSON(document); err != nil { + return fmt.Errorf("not a valid Sigstore bundle: %w", err) + } + envelope, err := b.Envelope() + if err != nil { + return fmt.Errorf("the Sigstore bundle signs a blob rather than an in-toto attestation") + } + a.bundle = &b + return a.parseEnvelope(envelope.RawEnvelope()) +} + +func (a *Attestation) parseEnvelope(envelope *dsse.Envelope) error { + if envelope.PayloadType != inTotoPayloadType { + return fmt.Errorf("the DSSE envelope carries %q, not an in-toto statement (%s)", envelope.PayloadType, inTotoPayloadType) + } + payload, err := envelope.DecodeB64Payload() + if err != nil { + return fmt.Errorf("the DSSE envelope's payload is not valid base64: %w", err) + } + a.envelope = envelope + return a.parseStatement(payload) +} + +func (a *Attestation) parseStatement(payload []byte) error { + statement := &ita1.Statement{} + if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(payload, statement); err != nil { + return fmt.Errorf("not a valid in-toto statement: %w", err) + } + if err := statement.Validate(); err != nil { + return fmt.Errorf("not a valid in-toto statement: %w", err) + } + var raw struct { + Predicate json.RawMessage `json:"predicate"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return fmt.Errorf("not a valid in-toto statement: %w", err) + } + a.Statement = statement + a.Predicate = raw.Predicate + return nil +} diff --git a/internal/intoto/read_test.go b/internal/intoto/read_test.go new file mode 100644 index 000000000..7bb82b452 --- /dev/null +++ b/internal/intoto/read_test.go @@ -0,0 +1,176 @@ +package intoto + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +const artifactDigest = "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8" + +func statementFixture(t *testing.T, name string) []byte { + t.Helper() + content, err := os.ReadFile(filepath.Join("testdata", "statements", name)) + require.NoError(t, err) + return content +} + +func compact(t *testing.T, content []byte) []byte { + t.Helper() + var out bytes.Buffer + require.NoError(t, json.Compact(&out, content)) + return out.Bytes() +} + +// envelope wraps a statement in a DSSE envelope. The signature is not a real +// one: reading never checks it. +func envelope(t *testing.T, payloadType string, statement []byte, signatures ...string) []byte { + t.Helper() + sigs := []map[string]string{} + for _, sig := range signatures { + sigs = append(sigs, map[string]string{"keyid": "", "sig": base64.StdEncoding.EncodeToString([]byte(sig))}) + } + content, err := json.Marshal(map[string]any{ + "payloadType": payloadType, + "payload": base64.StdEncoding.EncodeToString(statement), + "signatures": sigs, + }) + require.NoError(t, err) + return content +} + +func TestRead(t *testing.T) { + t.Run("reads a bare statement as one unsigned attestation", func(t *testing.T) { + content := statementFixture(t, "github-provenance.json") + + attestations, err := Read(content) + require.NoError(t, err) + require.Len(t, attestations, 1) + require.Equal(t, "https://slsa.dev/provenance/v1", attestations[0].Statement.GetPredicateType()) + require.False(t, attestations[0].Signed()) + require.Equal(t, 0, attestations[0].Position) + require.Equal(t, content, attestations[0].Raw, "Raw should be the whole file, so its checksum is the file's") + }) + + t.Run("reads a v0.1 statement", func(t *testing.T) { + attestations, err := Read(statementFixture(t, "slsa-v0.2-provenance.json")) + require.NoError(t, err) + require.Equal(t, "https://slsa.dev/provenance/v0.2", attestations[0].Statement.GetPredicateType()) + }) + + t.Run("keeps the predicate exactly as the payload carried it", func(t *testing.T) { + attestations, err := Read(statementFixture(t, "cyclonedx-sbom.json")) + require.NoError(t, err) + require.Contains(t, string(attestations[0].Predicate), `"version": 1,`) + }) + + t.Run("reads an unsigned DSSE envelope", func(t *testing.T) { + attestations, err := Read(envelope(t, inTotoPayloadType, statementFixture(t, "spdx-sbom.json"))) + require.NoError(t, err) + require.Len(t, attestations, 1) + require.Equal(t, "https://spdx.dev/Document/v2.3", attestations[0].Statement.GetPredicateType()) + require.False(t, attestations[0].Signed()) + }) + + t.Run("treats a DSSE envelope with a signature as signed", func(t *testing.T) { + attestations, err := Read(envelope(t, inTotoPayloadType, statementFixture(t, "spdx-sbom.json"), "signature")) + require.NoError(t, err) + require.True(t, attestations[0].Signed()) + }) + + t.Run("refuses a DSSE envelope that does not carry an in-toto statement", func(t *testing.T) { + _, err := Read(envelope(t, "application/vnd.example+json", statementFixture(t, "spdx-sbom.json"))) + require.EqualError(t, err, `the attestation: the DSSE envelope carries "application/vnd.example+json", not an in-toto statement (application/vnd.in-toto+json)`) + }) + + t.Run("reads one attestation per line, numbering them", func(t *testing.T) { + lines := [][]byte{ + compact(t, statementFixture(t, "vuln-scan.json")), + compact(t, envelope(t, inTotoPayloadType, statementFixture(t, "github-provenance.json"))), + compact(t, statementFixture(t, "cyclonedx-sbom.json")), + } + content := append(bytes.Join(lines, []byte("\n")), '\n') + + attestations, err := Read(content) + require.NoError(t, err) + require.Len(t, attestations, 3) + for i, a := range attestations { + require.Equal(t, i+1, a.Position) + require.Equal(t, lines[i], a.Raw, "Raw should be the line the attestation was read from") + } + require.Equal(t, "https://slsa.dev/provenance/v1", attestations[1].Statement.GetPredicateType()) + }) + + t.Run("names the line a bad attestation is on", func(t *testing.T) { + content := append(compact(t, statementFixture(t, "vuln-scan.json")), []byte("\n{\"hello\": \"world\"}\n")...) + _, err := Read(content) + require.EqualError(t, err, "attestation 2: not a Sigstore bundle, a DSSE envelope or an in-toto statement") + }) + + t.Run("refuses a file that is not JSON", func(t *testing.T) { + _, err := Read([]byte("SPDXVersion: SPDX-2.3\n")) + require.ErrorContains(t, err, "not valid JSON") + }) + + t.Run("refuses an empty file", func(t *testing.T) { + _, err := Read([]byte("\n")) + require.EqualError(t, err, "holds no attestation") + }) + + t.Run("refuses a statement with no subject", func(t *testing.T) { + _, err := Read([]byte(`{"_type": "https://in-toto.io/Statement/v1", "predicateType": "https://slsa.dev/provenance/v1", "predicate": {}}`)) + require.EqualError(t, err, "the attestation: not a valid in-toto statement: at least one subject required") + }) +} + +func TestReadEnvelope(t *testing.T) { + t.Run("reads a Sigstore bundle", func(t *testing.T) { + content, err := os.ReadFile(filepath.Join("testdata", "spdx-sbom.sigstore.json")) + require.NoError(t, err) + + attestation, wrapped, err := ReadEnvelope(content) + require.NoError(t, err) + require.True(t, wrapped) + require.True(t, attestation.Signed()) + require.Equal(t, content, attestation.Raw) + }) + + t.Run("reads a DSSE envelope", func(t *testing.T) { + attestation, wrapped, err := ReadEnvelope(envelope(t, inTotoPayloadType, statementFixture(t, "spdx-sbom.json"))) + require.NoError(t, err) + require.True(t, wrapped) + require.False(t, attestation.Signed()) + }) + + t.Run("leaves anything else, such as the SBOM itself, unwrapped", func(t *testing.T) { + for _, content := range []string{ + `{"bomFormat": "CycloneDX", "specVersion": "1.6"}`, + ``, + "SPDXVersion: SPDX-2.3\n", + string(statementFixture(t, "spdx-sbom.json")), + } { + _, wrapped, err := ReadEnvelope([]byte(content)) + require.NoError(t, err) + require.False(t, wrapped, content) + } + }) + + t.Run("refuses several attestations, which no single bundle or envelope can be", func(t *testing.T) { + content, err := os.ReadFile(filepath.Join("testdata", "attestations.intoto.jsonl")) + require.NoError(t, err) + + _, _, err = ReadEnvelope(content) + require.ErrorIs(t, err, ErrSeveralDocuments) + }) + + t.Run("reports a malformed envelope rather than treating it as an SBOM", func(t *testing.T) { + _, wrapped, err := ReadEnvelope([]byte(`{"payloadType": "application/vnd.in-toto+json", "payload": "not base64!", "signatures": []}`)) + require.True(t, wrapped) + require.ErrorContains(t, err, "the DSSE envelope's payload is not valid base64") + }) +} diff --git a/internal/intoto/sbom.go b/internal/intoto/sbom.go new file mode 100644 index 000000000..23429e4a4 --- /dev/null +++ b/internal/intoto/sbom.go @@ -0,0 +1,12 @@ +package intoto + +import "fmt" + +// SBOMDocument returns the SPDX or CycloneDX document an SBOM attestation +// carries, as the bytes the signed payload held, for internal/sbom to read. +func SBOMDocument(a *Attestation) ([]byte, error) { + if !KindSBOM.Matches(a.Statement.GetPredicateType()) { + return nil, fmt.Errorf("%s is %s, not an SPDX or CycloneDX SBOM", a.Describe(), a.Statement.GetPredicateType()) + } + return a.Predicate, nil +} diff --git a/internal/intoto/testdata/README.md b/internal/intoto/testdata/README.md new file mode 100644 index 000000000..e2250ff69 --- /dev/null +++ b/internal/intoto/testdata/README.md @@ -0,0 +1,41 @@ +# in-toto test fixtures + +Everything here works offline. The signed fixtures are checked against +`trusted_root.json`, not against Sigstore's public-good instance, so no test +fetches a trust root. + +## Regenerating + +```bash +REGENERATE_INTOTO_FIXTURES=1 go test ./internal/intoto -run TestRegenerateFixtures +``` + +`TestRegenerateFixtures` in `fixtures_test.go` signs the statements in +`statements/` with a throwaway Sigstore: a Fulcio authority issuing +certificates that name a GitHub Actions workflow, and the Rekor log and +timestamp authority of sigstore-go's `testing/ca`. Every run makes new keys, +so commit the regenerated bundles and `trusted_root.json` together. + +The bundles stay valid after the throwaway certificates expire, because a +Sigstore signature is checked against the time Rekor logged it. + +## What each fixture is for + +| Fixture | What it is | +| --- | --- | +| `artifact.bin` | The artifact every statement is about (sha256 `e889...60a8`). | +| `statements/*.json` | Unsigned statements, as buildx writes by default. The signed fixtures are made from these. | +| `trusted_root.json` | The trust root the signed fixtures verify against. | +| `github-provenance.sigstore.json` | SLSA v1 provenance from GitHub Actions (`actions/attest-build-provenance`), signed by `https://github.com/acme/widget/.github/workflows/build.yml@refs/heads/main`. | +| `buildkit-provenance.sigstore.json` | SLSA v1 provenance from BuildKit, signed by the same workflow. | +| `slsa-v0.2-provenance.sigstore.json` | SLSA v0.2 provenance from the SLSA GitHub generator. | +| `spdx-sbom.sigstore.json`, `cyclonedx-sbom.sigstore.json` | SBOM attestations (`actions/attest-sbom`). | +| `wrong-subject-provenance.sigstore.json` | Validly signed, but about another artifact. | +| `wrong-signer-provenance.sigstore.json` | Validly signed by `https://github.com/mallory/...`. | +| `tampered-provenance.sigstore.json` | The commit in the statement was changed after signing. | +| `attestations.intoto.jsonl` | Three signed bundles, one per line: a vulnerability scan, provenance and a CycloneDX SBOM. | +| `newest-first.intoto.jsonl` | Two signed provenance bundles; the first was logged later. | +| `vuln-only.intoto.jsonl` | Only vulnerability scans, one signed and one not. | +| `wrong-signer-and-unsigned.intoto.jsonl` | Provenance signed by `https://github.com/mallory/...`, then the same provenance unsigned. | +| `key-signed-provenance.dsse.json`, `key.pub` | A DSSE envelope signed with a key rather than a certificate, and that key. | +| `unsigned-spdx-sbom.dsse.json` | A DSSE envelope with no signatures. | diff --git a/internal/intoto/testdata/artifact.bin b/internal/intoto/testdata/artifact.bin new file mode 100644 index 000000000..06c5265fa --- /dev/null +++ b/internal/intoto/testdata/artifact.bin @@ -0,0 +1 @@ +kosli in-toto test artifact diff --git a/internal/intoto/testdata/attestations.intoto.jsonl b/internal/intoto/testdata/attestations.intoto.jsonl new file mode 100644 index 000000000..a630b0d10 --- /dev/null +++ b/internal/intoto/testdata/attestations.intoto.jsonl @@ -0,0 +1,3 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzzCCAnSgAwIBAgIIAfU8kL7ApH4wCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEspYbAx8Vi3bRp8GH8N7ME8Y/8joJ3NrUpmYoNTnawfxbhBkyo5OlAQXXMqK3JE7TKApWGYMuO3TkkMviC+iQU6OCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0kAMEYCIQCo3kg4XhLFZZ0QYtB5JXHZCWe3+/VGw0aen6SZaj1D9QIhAKGCpDiIQk1XWAT7thvnz2rFbJDaitJbwFdJa64BVEQl"},"tlogEntries":[{"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1794395512","inclusionPromise":{"signedEntryTimestamp":"MEUCIQCf83JLChpeU4yB1frDvWMjiBg9h+AxmqLqLxYvCCT5ZAIgJ/Qwa6BCJiKOJ5FuivmJyJi2LQwlposGsawv2YVRyLU="},"inclusionProof":{"rootHash":"GgOIFszWyNOrW9hJIFk/fV8qv4nOLfxu76QA0FD8EfQ=","treeSize":"1","checkpoint":{"envelope":"rekor.localhost - 123\n42\nGgOIFszWyNOrW9hJIFk/fV8qv4nOLfxu76QA0FD8EfQ=\n\n— rekor.localhost nk5pvzBGAiEAx5RP6panHUYhKiehhnmLKxHcvEKnnDYcblHCjVgypTICIQCEAVL9o5N1Jiu6f/GSXMGT54O8v/QUnyhxk2Yktmh/pg==\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImI1OWEyMjQ4ZDZkZDcxMGRlZjg1MzQyNjZmNTZhZWUwNTgwN2MzZDc3NmUyMDdjMGQ5MjQyOTZmOTY0ZWFmNTkifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJiM2Q0NTk3OWYwMTRjNTAwNDdmOTMyZWM4NWYxYzFlZGQ1YjY4YWRiMmRiYTMwMzUxMDYzNTFjMzZjNWRhNmZmIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJR3FiTmMzZDRNdTVVV0ZIdmxIOTFNQ1NRQW1vQit5Vmk0VzBuUUY1MGViekFpRUE1NXNzSW93TDZEWmVjZ3VpRFEzcmZWM24xWjNYUzNLd08yTkhOOERSdEVZPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZla05EUVc1VFowRjNTVUpCWjBsSlFXWlZPR3RNTjBGd1NEUjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlhOd1dXSkJlRGhXYVROaVVuQTRSMGc0VGpkTlJUaFpMemhxYjBvelRuSlZjRzFaYjA1VWJtRjNabmhpYUVKcmVXODFUMndLUVZGWVdFMXhTek5LUlRkVVMwRndWMGRaVFhWUE0xUnJhMDEyYVVNcmFWRlZOazlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR3RCVFVWWlEwbFJRMjh6YTJjMFdHaE1SbHBhTUZGWmRFSTFTbGhJV2dwRFYyVXpLeTlXUjNjd1lXVnVObE5hWVdveFJEbFJTV2hCUzBkRGNFUnBTVkZyTVZoWFFWUTNkR2gyYm5veWNrWmlTa1JoYVhSS1luZEdaRXBoTmpSQ0NsWkZVV3dLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICUjADAgEAMIICSQYJKoZIhvcNAQcCoIICOjCCAjYCAQMxDTALBglghkgBZQMEAgEwgYIGCyqGSIb3DQEJEAEEoHMEcTBvAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgqWPsTWZ3STeyA7IuTZvGF0xQRi7IQ/Ex7S87nlM0beoCFQDjO57G8mcVSDKX/DkO39FdZ7LfERgPMjAyNjExMTExMTEwNTJaoASkAjAAoAAxggGZMIIBlQIBATBAMDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATALBglghkgBZQMEAgGggeowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjExMTExMTEwNTJaMC8GCSqGSIb3DQEJBDEiBCBdb8ku9sqAIiWO+TsOIqXRvWJIaOpQptA3zw811LDz8zB9BgsqhkiG9w0BCRACLzFuMGwwajBoBCBUqoykfHUXJTYsvFAHbO71/Km+1YCSTyMpDoJuO0Fz5jBEMD+kPTA7MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIjAgBgNVBAMTGXNpZ3N0b3JlLXRzYS1pbnRlcm1lZGlhdGUCAQEwCgYIKoZIzj0EAwIESDBGAiEApqkRHeDRBKqVwRtxatPpmDpOhsMnmzSD7YlVU/dtPM0CIQDWc6wDUzuuIRFt/BT4sSfJ/T+zTzM/uVIgRFQh/10HyA=="}]}},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9jb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3Z1bG4vdjEiLCJwcmVkaWNhdGUiOnsiaW52b2NhdGlvbiI6eyJ1cmkiOiJodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvYWN0aW9ucy9ydW5zLzEyMzQ1Njc4OTAifSwic2Nhbm5lciI6eyJ1cmkiOiJwa2c6Z2l0aHViL2FxdWFzZWN1cml0eS90cml2eUAwLjUwLjAiLCJ2ZXJzaW9uIjoiMC41MC4wIiwicmVzdWx0Ijp7fX0sIm1ldGFkYXRhIjp7InNjYW5TdGFydGVkT24iOiIyMDI2LTExLTAxVDA5OjAwOjAwWiIsInNjYW5GaW5pc2hlZE9uIjoiMjAyNi0xMS0wMVQwOTowMTowMFoifX19","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEUCIGqbNc3d4Mu5UWFHvlH91MCSQAmoB+yVi4W0nQF50ebzAiEA55ssIowL6DZecguiDQ3rfV3n1Z3XS3KwO2NHN8DRtEY="}]}} +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzjCCAnSgAwIBAgIIB6Rw0scmsfkwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVDU29ejeL6OwkmfvDl3aQfTptHPiC0jhR5fYNauz7rAb+I59AfEsBMiNUvd+eteN22EXjCg+3pyZJDHqV8TmqKOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIHXnan7JZT98HLxCMPOdkCWbACcdb6CXwZ3Lpcir9zx+AiEAqymXO5VlEyjj2VoxferUKkEOuBRacGa7YvIE+GyNyww="},"tlogEntries":[{"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1794395512","inclusionPromise":{"signedEntryTimestamp":"MEUCIQD2PLP8yFGb+oTid0krrGgJpXcD0qfUqVaKo0BlHFE7rgIgcJmfkyZPuWKqtMVugHIeiwvOc4a2EZGrmmKw8BOeDg4="},"inclusionProof":{"rootHash":"o3ZCk2mIBJR1jE0I/dU8um6WPBIItcwn3zqZK0nCEOw=","treeSize":"1","checkpoint":{"envelope":"rekor.localhost - 123\n42\no3ZCk2mIBJR1jE0I/dU8um6WPBIItcwn3zqZK0nCEOw=\n\n— rekor.localhost nk5pvzBEAiAWOuv0OQQPDsBdVWN4ds1m4c4bN9B879GYd6TSiOvrEAIgKPL/H51Aa6388e45ISoSoHqXpYNpe7KvIMWWmsXBWAQ=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6Ijg2MTUzYTNmYmQ4ZDEyYjMyM2RlM2Q1NTM0MTQ0YWY2NWJmMTQ3OGU5MDdmMDE1MTZiMTQ2NmNiNDBhM2YxOTIifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FWUNJUUNHRXlGYjJhMDZRWmUxYVkyNG1xVkYySjhaMUFVZVVYMEgwSndhVFJyTDV3SWhBUEMrZ3l0N3UrVnYzRzdaQ0VCVEJzNjBsMmdtRHQ3VW0vNEZtU0NtKytHQiIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZha05EUVc1VFowRjNTVUpCWjBsSlFqWlNkekJ6WTIxelptdDNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlZaRVZUSTVaV3BsVERaUGQydHRablpFYkROaFVXWlVjSFJJVUdsRE1HcG9ValZtV1U1aGRYbzNja0ZpSzBrMU9VRm1SWE1LUWsxcFRsVjJaQ3RsZEdWT01qSkZXR3BEWnlzemNIbGFTa1JJY1ZZNFZHMXhTMDlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbElXRzVoYmpkS1dsUTVPRWhNZUVOTlVFOWthME5YWWdwQlEyTmtZalpEV0hkYU0weHdZMmx5T1hwNEswRnBSVUZ4ZVcxWVR6VldiRVY1YW1veVZtOTRabVZ5VlV0clJVOTFRbEpoWTBkaE4xbDJTVVVyUjNsT0NubDNkejBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICUTADAgEAMIICSAYJKoZIhvcNAQcCoIICOTCCAjUCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgRrFA6QFjQKKe2GOm+/Nm5tIGSqz20nsISQsm23w+ZjoCFEGjXkwoMDMc4iIHD9ZhKukCzzwNGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZkwggGVAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIMIMwzyWlDawpiQH+K4IIjaCWTJW8YkXebO+h0hHO8RLMH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRIMEYCIQDNpLDaLksyEHF28VAndJ61YdQeDLLdCDreyjhN6rZ2dQIhANMGhTvCM9zRqsj6oDPhcsLfsR/BUrc9/sCLnJsoSIwE"}]}},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEYCIQCGEyFb2a06QZe1aY24mqVF2J8Z1AUeUX0H0JwaTRrL5wIhAPC+gyt7u+Vv3G7ZCEBTBs60l2gmDt7Um/4FmSCm++GB"}]}} +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzTCCAnSgAwIBAgIIOcfo2laJvRswCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPRCXujZoSoVpS6fJxvpoIZtas6ZKALUBn1hE0umyjXcNwOpNeiwKriOvUzQRSzFUFvpzhHlAzcPg3bEupwApD6OCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0cAMEQCIB772vYSJK5tarokIEZ9HJMPOJssLrWqn9aSg96DuoIhAiByggMlqV3GeoUJweVy4iTo3EtdE2V0Dv3km1MAJoSWTw=="},"tlogEntries":[{"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1794395512","inclusionPromise":{"signedEntryTimestamp":"MEYCIQCcWGNDLbp6nW20bgNlq9JDOCrVCfz07xyzV8OqXKZIrQIhAJ/i4gjJlUEqpAfmPaPzNCpVLSfg2gg5v/pdtbT3n/IP"},"inclusionProof":{"rootHash":"Ab6jKfA3YHv9CxuY/elXni31GYuxhKvuRrx6ex1M8RI=","treeSize":"1","checkpoint":{"envelope":"rekor.localhost - 123\n42\nAb6jKfA3YHv9CxuY/elXni31GYuxhKvuRrx6ex1M8RI=\n\n— rekor.localhost nk5pvzBEAiAwKCFJGW92roiqEbQfpHTqFssIlQ9CCgUXoqlYr0HPOwIgZOdZJJLmnWMrCWCAch0rETzSYkXFIf2HFlYDcx/h+w0=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjcxYjgwOGFhYzRmMGNhNGQxYTRkNDFkYjVmNmIyOGQ1OTFhYmZiMTIyMTM5MzU1YTNmMWI2M2ZmMTNlMDcwODcifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyMmQ2OGMxNmVhMTg2ZDIwNjk1YzVhMGY2NTgzYTNhNTM5NDVmMDg2YjU1OWUzZDE1Yjg1YjQ5NjVhOGIyZTk1In0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FUUNJSDQ4QXllZVhqcmxpNUw0aDB3dzB4WVZsTGFyRGpOVHQ0S1JxdXZiNTNsMEFpQmloZGdjUVhid1FNaDFWTVdyM3lrbjVOTm9LSG9vWUIxVjcyZC9XWFZVMmc9PSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZWRU5EUVc1VFowRjNTVUpCWjBsSlQyTm1iekpzWVVwMlVuTjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlZCU1ExaDFhbHB2VTI5V2NGTTJaa3A0ZG5CdlNWcDBZWE0yV2t0QlRGVkNiakZvUlRCMWJYbHFXR05PZDA5d1RtVnBkMHNLY21sUGRsVjZVVkpUZWtaVlJuWndlbWhJYkVGNlkxQm5NMkpGZFhCM1FYQkVOazlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR05CVFVWUlEwbENOemN5ZGxsVFNrczFkR0Z5YjJ0SlJWbzVTRXBOVUFwUFNuTnpUSEpYY1c0NVlWTm5PVFpFZFc5SmFFRnBRbmxuWjAxc2NWWXpSMlZ2VlVwM1pWWjVOR2xVYnpORmRHUkZNbFl3UkhZemEyMHhUVUZLYjFOWENsUjNQVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICUDADAgEAMIICRwYJKoZIhvcNAQcCoIICODCCAjQCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgr6uo8vLTqYcewWNNYyGtP4BIyFeXE96yS64CZEUotf0CFAaLbnaMOsjacSbaGU9FuVEQ5I+MGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZgwggGUAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIGb/NSTeEJrJXXwMO7p+XWAtD5ooQVwkGtWOpmgEzaU6MH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRHMEUCIQCSh6bZPS7Zbs4K3u6Az95jVjMb4BGCRMx3nd6EagLfMwIgWan/oQINLGmHlr6ewiK68jX+3VYr0I+FR2cxtOfczOo="}]}},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9jeWNsb25lZHgub3JnL2JvbSIsInByZWRpY2F0ZSI6eyJib21Gb3JtYXQiOiJDeWNsb25lRFgiLCJzcGVjVmVyc2lvbiI6IjEuNiIsInZlcnNpb24iOjEsIm1ldGFkYXRhIjp7InRpbWVzdGFtcCI6IjIwMjYtMTEtMDFUMDk6MDA6MDBaIiwidG9vbHMiOnsiY29tcG9uZW50cyI6W3sidHlwZSI6ImFwcGxpY2F0aW9uIiwibmFtZSI6ImV4YW1wbGUtc2JvbS10b29sIiwidmVyc2lvbiI6IjEuMCJ9XX0sImNvbXBvbmVudCI6eyJ0eXBlIjoiYXBwbGljYXRpb24iLCJuYW1lIjoiYXJ0aWZhY3QuYmluIiwidmVyc2lvbiI6IjEuMi4zIn19LCJjb21wb25lbnRzIjpbeyJ0eXBlIjoibGlicmFyeSIsIm5hbWUiOiJsZWZ0LXBhZCIsInZlcnNpb24iOiIxLjMuMCIsInB1cmwiOiJwa2c6bnBtL2xlZnQtcGFkQDEuMy4wIn1dfX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEQCIH48AyeeXjrli5L4h0ww0xYVlLarDjNTt4KRquvb53l0AiBihdgcQXbwQMh1VMWr3ykn5NNoKHooYB1V72d/WXVU2g=="}]}} diff --git a/internal/intoto/testdata/buildkit-provenance.sigstore.json b/internal/intoto/testdata/buildkit-provenance.sigstore.json new file mode 100644 index 000000000..3816150df --- /dev/null +++ b/internal/intoto/testdata/buildkit-provenance.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOmRvY2tlci9hY21lL3dpZGdldEAxLjIuMz9wbGF0Zm9ybT1saW51eCUyRmFtZDY0IiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2dpdGh1Yi5jb20vbW9ieS9idWlsZGtpdC9ibG9iL21hc3Rlci9kb2NzL2F0dGVzdGF0aW9ucy9zbHNhLWRlZmluaXRpb25zLm1kIiwiZXh0ZXJuYWxQYXJhbWV0ZXJzIjp7ImNvbmZpZ1NvdXJjZSI6eyJ1cmkiOiJodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQuZ2l0I3JlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJzaGExIjoiNGY5ZDJhN2MxZTZiOGEzZDVmMGUyYzliN2ExZDRlNmY4YzNiNWE3MCJ9LCJwYXRoIjoiRG9ja2VyZmlsZSJ9LCJyZXF1ZXN0Ijp7ImZyb250ZW5kIjoiZG9ja2VyZmlsZS52MCIsImFyZ3MiOnt9fX0sImludGVybmFsUGFyYW1ldGVycyI6eyJidWlsZGVyUGxhdGZvcm0iOiJsaW51eC9hbWQ2NCJ9LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoicGtnOmRvY2tlci9hbHBpbmVAMy4yMD9wbGF0Zm9ybT1saW51eCUyRmFtZDY0IiwiZGlnZXN0Ijp7InNoYTI1NiI6IjBmMmQ1YzM4ZGQ3YTRmNGY3MzNlNjg4ZTNhNjczM2NiNWFiMWFjNmUzY2I0NjAzYTVkZDU2NGU1YmZiODBlZWQifX0seyJ1cmkiOiJodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQuZ2l0I3JlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJzaGExIjoiNGY5ZDJhN2MxZTZiOGEzZDVmMGUyYzliN2ExZDRlNmY4YzNiNWE3MCJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvYWN0aW9ucy9ydW5zLzEyMzQ1Njc4OTAvYXR0ZW1wdHMvMSJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSUQiOiJycHY3YTM4OXV6aWw1bHFtcmd3aGlqd2p6IiwiYnVpbGRraXRfbWV0YWRhdGEiOnsidmNzIjp7InNvdXJjZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInJldmlzaW9uIjoiNGY5ZDJhN2MxZTZiOGEzZDVmMGUyYzliN2ExZDRlNmY4YzNiNWE3MCJ9fX19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEYCIQCgnTyfZt3oEKYvztyXKaYeUPzXFmspgbpV4VxkGN3EGwIhAIxndxQR30dB7mYJism0pRzG0KgUdzmudjlFWrIVreVp" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzTCCAnSgAwIBAgIIBjZkPGzOoBUwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExdVuBza+f0UFY3EiibUH0CMR99cENgBCTIgumcsNrujnpf2VH6T+sxm1SmcSZK2kckCk2Ovm0FdE0EIc4vv/A6OCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0cAMEQCICNxiXR3zNSTfPC2oQfgEq2Zy08l6b4r0xP2b5Y9/dvgAiB8zNUldB+Gd1/LjJhCD2jx0jRnrFopTBf8RD8Ne+Gm9A==" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUTADAgEAMIICSAYJKoZIhvcNAQcCoIICOTCCAjUCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg/GE/87Oprq4fvZkBKSLdXVbz1LxywnLVeMN1MYbktEkCFGP7EGQbREIlWIEdE3i62SSErHRHGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZkwggGVAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIOc10FucrXeU0x1t4xY0giFYm6hDtv1z5q3vMinMRM60MH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRIMEYCIQCG3unafBXflwl+eewjNemKJiAsY7fcubkc74PcCFX9MwIhAI4tGJXmm0C7pTlgz4tl7mLxaf3Knj9cr1Gx0rTWX44s" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjliMWExNmVkYjdiMGMyY2JkOWJhM2NkOTNkM2NhYzQ1YzA3ZGMyYTc1ZDlmOTk2NmJhYjEyZTEyYjM4NzI1ZmYifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyZmY0MzYzYzU1NmQ0YmNiYTdkOTEwMjBlNTMyNTY0NDc3N2E1MmJlMGIwZjQwZWEzMmU1YWIwMTkzM2E3MTI1In0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FWUNJUUNnblR5Zlp0M29FS1l2enR5WEthWWVVUHpYRm1zcGdicFY0VnhrR04zRUd3SWhBSXhuZHhRUjMwZEI3bVlKaXNtMHBSekcwS2dVZHptdWRqbEZXcklWcmVWcCIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZWRU5EUVc1VFowRjNTVUpCWjBsSlFtcGFhMUJIZWs5dlFsVjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlhoa1ZuVkNlbUVyWmpCVlJsa3pSV2xwWWxWSU1FTk5Vams1WTBWT1owSkRWRWxuZFcxamMwNXlkV3B1Y0dZeVZrZzJWQ3NLYzNodE1WTnRZMU5hU3pKclkydERhekpQZG0wd1JtUkZNRVZKWXpSMmRpOUJOazlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR05CVFVWUlEwbERUbmhwV0ZJemVrNVRWR1pRUXpKdlVXWm5SWEV5V2dwNU1EaHNObUkwY2pCNFVESmlOVms1TDJSMlowRnBRamg2VGxWc1pFSXJSMlF4TDB4cVNtaERSREpxZURCcVVtNXlSbTl3VkVKbU9GSkVPRTVsSzBkdENqbEJQVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIDyIG5l5iJDaTWStjKv0amDnl2tJyTPKGPrmZpIMo8gWAiEA++PChZSJt9QK0lUmQ2ERPzaUImSbp0timIT822f2iRA=" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nb7ZkBp3Ika/3HxehdO/Jg7f1PAV172YG1AiBrDfNvqU=\n\n— rekor.localhost nk5pvzBGAiEA1wXY8CAWSuIHwEmP73He5Pilh20HCSHJBnQGZzu4jVwCIQDDi3rWzkFiz+zPaCa0+WS68R09ZdS64QJfUkhUzJ4etg==\n" + }, + "rootHash": "b7ZkBp3Ika/3HxehdO/Jg7f1PAV172YG1AiBrDfNvqU=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/cyclonedx-sbom.sigstore.json b/internal/intoto/testdata/cyclonedx-sbom.sigstore.json new file mode 100644 index 000000000..c2eb7d939 --- /dev/null +++ b/internal/intoto/testdata/cyclonedx-sbom.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9jeWNsb25lZHgub3JnL2JvbSIsInByZWRpY2F0ZSI6eyJib21Gb3JtYXQiOiJDeWNsb25lRFgiLCJzcGVjVmVyc2lvbiI6IjEuNiIsInZlcnNpb24iOjEsIm1ldGFkYXRhIjp7InRpbWVzdGFtcCI6IjIwMjYtMTEtMDFUMDk6MDA6MDBaIiwidG9vbHMiOnsiY29tcG9uZW50cyI6W3sidHlwZSI6ImFwcGxpY2F0aW9uIiwibmFtZSI6ImV4YW1wbGUtc2JvbS10b29sIiwidmVyc2lvbiI6IjEuMCJ9XX0sImNvbXBvbmVudCI6eyJ0eXBlIjoiYXBwbGljYXRpb24iLCJuYW1lIjoiYXJ0aWZhY3QuYmluIiwidmVyc2lvbiI6IjEuMi4zIn19LCJjb21wb25lbnRzIjpbeyJ0eXBlIjoibGlicmFyeSIsIm5hbWUiOiJsZWZ0LXBhZCIsInZlcnNpb24iOiIxLjMuMCIsInB1cmwiOiJwa2c6bnBtL2xlZnQtcGFkQDEuMy4wIn1dfX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQCPDb2Ja+7V/x7RP+wry1E/57sfzWpi0HNlTSjP+7nqtAIgMerA8Js86LNRZW941mnxrc80Hi6MLlCbAWT7IfKezYA=" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzjCCAnSgAwIBAgIIIsIocf2mfqUwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOR8qmCyTfBuMKi/X6FlnfNNxmVikkstET3Tod8Fqq7UZNkDlL0jt9j8TI7SZ/IEbosMqidewzBWwo86vjkqLkKOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIBKZJriMW8eN6sGq1N3IlciMARC1SwNIcpu74OYAGd7VAiEA/IayapU3LcLYS8QGflc6BxSVS+lBkBpdzggOlxeQgA4=" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUTADAgEAMIICSAYJKoZIhvcNAQcCoIICOTCCAjUCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgmujmLYsK/9qCIMCk+K79oj9L8LzINOGAyuGLznpOu5cCFED5basRQAhkcFXUmPALc1jwEfAmGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZkwggGVAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIJ+xc1um86OMvth39KZ5fHgj8KLYsPxovfKn5LBMhXMfMH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRIMEYCIQCBm+Y5zTX+xwZYpSYkHiyaG2SE6z9YUHlM0MXx6BdD/QIhAMSNqnxCo+kEeM/g2OcOp3j2CameTSKRe91NpIXvqXW2" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImU4NzdmY2UyMDU5MGVkMmViZDQ3N2Q1OGI0MmI4OTEzNzcwY2U3MDUyNmNkMmQzZTc0MjRjOGUzYTk5ZGMxZmEifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyMmQ2OGMxNmVhMTg2ZDIwNjk1YzVhMGY2NTgzYTNhNTM5NDVmMDg2YjU1OWUzZDE1Yjg1YjQ5NjVhOGIyZTk1In0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJUUNQRGIySmErN1YveDdSUCt3cnkxRS81N3NmeldwaTBITmxUU2pQKzducXRBSWdNZXJBOEpzODZMTlJaVzk0MW1ueHJjODBIaTZNTGxDYkFXVDdJZktlellBPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZha05EUVc1VFowRjNTVUpCWjBsSlNYTkpiMk5tTW0xbWNWVjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlU5U09IRnRRM2xVWmtKMVRVdHBMMWcyUm14dVprNU9lRzFXYVd0cmMzUkZWRE5VYjJRNFJuRnhOMVZhVG10RWJFd3dhblFLT1dvNFZFazNVMW92U1VWaWIzTk5jV2xrWlhkNlFsZDNiemcyZG1wcmNVeHJTMDlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbENTMXBLY21sTlZ6aGxUalp6UjNFeFRqTkpiR05wVFFwQlVrTXhVM2RPU1dOd2RUYzBUMWxCUjJRM1ZrRnBSVUV2U1dGNVlYQlZNMHhqVEZsVE9GRkhabXhqTmtKNFUxWlRLMnhDYTBKd1pIcG5aMDlzZUdWUkNtZEJORDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIG33HZsilpJFRF2mzvSYuadGyCK0OoS32qsuPYRhgqLfAiEAjCdFG1uhfIP3DnMhEUDnRVfYYiqnVJvpLDPbSqeItZE=" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nXnZ3tie06GJs8XVBo/H1WS8GInrRhVyAqElO58rlA6c=\n\n— rekor.localhost nk5pvzBFAiAewVeHjHHyWx2HBewcf2q9eFz8r7oZPUVq3e1CuPCgqQIhAKaqPujLnmVFXRrsB6VuHKAKFuoDtP8fEvQJsXQkiUpT\n" + }, + "rootHash": "XnZ3tie06GJs8XVBo/H1WS8GInrRhVyAqElO58rlA6c=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/github-provenance.sigstore.json b/internal/intoto/testdata/github-provenance.sigstore.json new file mode 100644 index 000000000..3b2544364 --- /dev/null +++ b/internal/intoto/testdata/github-provenance.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQCJxafAtLJQ+VznB5jjNAXRVnI2n0fiyB5Ds78PWapsawIgPWuDuAgb2wprcipWiX6k7YJaI2BKMrJIvvszFF+glZc=" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzjCCAnSgAwIBAgIIJW8IdsQ2XOIwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENUtLJ+l3BHVkybxb0kEpHhY1HBHePKYY3iUgiDMs+lm+HyXUq2Fva3CACqzSgZsnxtn6qWXPD5IpeiN2zVHpT6OCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIF/JlY2lSkuHPUNDnKIG1T3bkDmHqhXhBVRbJ0ay8H4EAiEAgxiBaQRdCZIZA127jUSX/xjLP94acREIdPOjLMCevMo=" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUDADAgEAMIICRwYJKoZIhvcNAQcCoIICODCCAjQCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg6hu8kzEzPRailbCXF7cjKA7pt1enja2+q/FYtt5mpxsCFAZHw7zRh8HI1veJFA7patMJ3Qc6GA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZgwggGUAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIE/yi/GlgFWmHGpbzLWuhUC7Pc5uV1Jo+2HLaQhcBf70MH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRHMEUCIQCKUC9QkGPVbAQ/4VfX4VRa9sOfBPM8Dr1x5E3hbtj0VgIgfDbS6niPkiFW06rUU8s5pPGOrqEOc6T2OejOo1q/JCY=" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjdjYzZiNjc4NDNiMmVhMjczYTljZTE4OTc1YzBiOWRjNTY4MWY2MGViZDhkYjljNTU4NmU5MmE4MmQwMzBiZmEifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJUUNKeGFmQXRMSlErVnpuQjVqak5BWFJWbkkybjBmaXlCNURzNzhQV2Fwc2F3SWdQV3VEdUFnYjJ3cHJjaXBXaVg2azdZSmFJMkJLTXJKSXZ2c3pGRitnbFpjPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZha05EUVc1VFowRjNTVUpCWjBsSlNsYzRTV1J6VVRKWVQwbDNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlU1VmRFeEtLMnd6UWtoV2EzbGllR0l3YTBWd1NHaFpNVWhDU0dWUVMxbFpNMmxWWjJsRVRYTXJiRzByU0hsWVZYRXlSbllLWVRORFFVTnhlbE5uV25OdWVIUnVObkZYV0ZCRU5VbHdaV2xPTW5wV1NIQlVOazlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbEdMMHBzV1RKc1UydDFTRkJWVGtSdVMwbEhNVlF6WWdwclJHMUljV2hZYUVKV1VtSktNR0Y1T0VnMFJVRnBSVUZuZUdsQ1lWRlNaRU5hU1ZwQk1USTNhbFZUV0M5NGFreFFPVFJoWTFKRlNXUlFUMnBNVFVObENuWk5iejBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEYCIQDVD4ZEzg11M6UNGDS+iUeAFIJwp1eKRwgL3Rg4y4sKqwIhAO1nuzk4/gdytuqgrki92fUwyaOAvEHnK6TOxH1S31Uw" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nYZXAT8oOS8VnKDJm+MMR6H4RqOGvz6iR+mMNMw5HlvU=\n\n— rekor.localhost nk5pvzBFAiEAl/f/4x6QyPfsXYmW6UnKpADQA8FGUKqFxKClHLGVDVECIFstwGRK96DhxvIn3AIleZ7hLXWZeuPXtI24scbkq5xp\n" + }, + "rootHash": "YZXAT8oOS8VnKDJm+MMR6H4RqOGvz6iR+mMNMw5HlvU=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/key-signed-provenance.dsse.json b/internal/intoto/testdata/key-signed-provenance.dsse.json new file mode 100644 index 000000000..1947c7cfe --- /dev/null +++ b/internal/intoto/testdata/key-signed-provenance.dsse.json @@ -0,0 +1,10 @@ +{ + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "keyid": "", + "sig": "MEUCIFWnbMzQV7LAEVQGYzEUX+emGCgL2RgU3GgElLMhWID+AiEAkFBQIdCdQsMX+A23+H8lXAzpc3UXQT4iQ+1UrFseeBA=" + } + ] +} diff --git a/internal/intoto/testdata/key.pub b/internal/intoto/testdata/key.pub new file mode 100644 index 000000000..3315c9b6d --- /dev/null +++ b/internal/intoto/testdata/key.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7vmHbWYuEBCXGfBtazgS7skRiqN4 +HsB/7yfNOIEGaDjwyrzA04WTktusBFkrQHZGE2sqcobS9Ad8NmSIBq5Azw== +-----END PUBLIC KEY----- diff --git a/internal/intoto/testdata/newest-first.intoto.jsonl b/internal/intoto/testdata/newest-first.intoto.jsonl new file mode 100644 index 000000000..5cea7e6c0 --- /dev/null +++ b/internal/intoto/testdata/newest-first.intoto.jsonl @@ -0,0 +1,2 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzzCCAnSgAwIBAgIICQw2Z7Sjb1kwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb6vm6fv2ryaheN/rooAwv1ycTtmhuALeiUkaSwC8tQzpYZcXEGRvsT7AEXGkEPYa/O5Rgzu71WNCWMfCVqRHkaOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0kAMEYCIQDwaoIk4n9T9XYNXioFHlpicubsjVnbo+kDj1DhncS3IQIhAMtDigvv7uJ9SpmUfDXD9TPYymRDQ9VFdrstP+uEGnko"},"tlogEntries":[{"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1794395632","inclusionPromise":{"signedEntryTimestamp":"MEUCIQCGpl2dlGY+VlRrD4JmgHa6PDiCUZbWqupViIyojVd8EwIgWNeckn2SOig4RnE/euXcxeHS8nZudBKrJapApT0eNvU="},"inclusionProof":{"rootHash":"Y/Gq/bYSGrWdqigpnHIPteQ6XKGIV59ljRnD48B35Ng=","treeSize":"1","checkpoint":{"envelope":"rekor.localhost - 123\n42\nY/Gq/bYSGrWdqigpnHIPteQ6XKGIV59ljRnD48B35Ng=\n\n— rekor.localhost nk5pvzBFAiBQlpbAoZjr2ov/1941Vdw+jwbMJyGTMkyMcYkAMy9zcQIhAPGg8ofMO0CjTGXJ8N1xjziqHXx8eR18inapzPLOL06Y\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjQzMTFkMGE5OGRjZDU0MGU5YmFmYjMxZjIzNGIxOTEzNWZlNDQzN2I5ZjE2Y2ZjNjQzYjIyMzVlMzA0YzljNjcifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FWUNJUUN5R3FPRlZnYktFYmZkalJIYWJpUzg2aVdQMW5LcGFyUzZKbnhuYWU5MEVBSWhBTUlTSmtISTdYdSs1MFptVk9JZno4eE5acENVUnd5OEVlYll3RzlaK2RDLyIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZla05EUVc1VFowRjNTVUpCWjBsSlExRjNNbG8zVTJwaU1XdDNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUldJMmRtMDJabll5Y25saGFHVk9MM0p2YjBGM2RqRjVZMVIwYldoMVFVeGxhVlZyWVZOM1F6aDBVWHB3V1ZwaldFVkhVbllLYzFRM1FVVllSMnRGVUZsaEwwODFVbWQ2ZFRjeFYwNURWMDFtUTFaeFVraHJZVTlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR3RCVFVWWlEwbFJSSGRoYjBsck5HNDVWRGxZV1U1WWFXOUdTR3h3YVFwamRXSnphbFp1WW04cmEwUnFNVVJvYm1OVE0wbFJTV2hCVFhSRWFXZDJkamQxU2psVGNHMVZaa1JZUkRsVVVGbDViVkpFVVRsV1JtUnljM1JRSzNWRkNrZHVhMjhLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICUTADAgEAMIICSAYJKoZIhvcNAQcCoIICOTCCAjUCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgojpFgbO340XHfrAjR98Q7wObq1eHDI2vKybzdkXne+ICFDx5Jg9eB1WH5wsvZCoxAlNk6Mb3GA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZkwggGVAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIOWgIsazTQ8P5rA/4fCuQ3BlJTFhLG5u5Yoj0Uj1+YUNMH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRIMEYCIQCGGqWL23nQnYAJ8Gp5KrZVko9/tfP5jlZH5aJB09ZDngIhAM0UKL8RfumiJAlNEkBUTP+1GLqkwP48XCFDo3syqM7v"}]}},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEYCIQCyGqOFVgbKEbfdjRHabiS86iWP1nKparS6Jnxnae90EAIhAMISJkHI7Xu+50ZmVOIfz8xNZpCURwy8EebYwG9Z+dC/"}]}} +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzTCCAnSgAwIBAgIIM5VBHN2mq88wCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE52cGaX+zb7X+1+YKpS3olDb+BAuwL1PC5lxlnbRxWYH41r3jNtxPEy73wbE5lRo1ijoTyLXoxIjk/DMXz/8a/6OCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0cAMEQCIGXeEs0/zqjS2e1sfdk+yiP3VPYV/OihV2xT684Ne+UlAiBg3ZE6rzvxOQOERxscEJ7ZDuwBoOm7Mp3a9VRYXL6z4Q=="},"tlogEntries":[{"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1794395572","inclusionPromise":{"signedEntryTimestamp":"MEUCIQDtzoG93UA2k7CA73mFSdL1tRGvUl0yYKlIbVgjAmdBJgIgW/arFB9Ry5lPivtp50M71OM6o1L6r5qgvGL/2lnT+hg="},"inclusionProof":{"rootHash":"Ud2MJ3S1Tw6LlWQ7dUMJiT1E4+rxwUezWHESVDBAhWo=","treeSize":"1","checkpoint":{"envelope":"rekor.localhost - 123\n42\nUd2MJ3S1Tw6LlWQ7dUMJiT1E4+rxwUezWHESVDBAhWo=\n\n— rekor.localhost nk5pvzBEAiA8ZzbURfxeejuD+KBmYEG9PKkPFho1iJ7KWgrJoOMfjQIgN/mPGPIz+ZENop4VnuNbajZreO09lCMeKphBCV0XWmA=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjA3NTEzN2I4MzEyZjk2MzY0NDE2YmM5M2I0NzVhM2M1MTYwYzM0YzI3N2Q0MTQ5M2RmZGI5ZTJjODZiOWQxYTAifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJUURqVWIvQXpHU0xvU2tnMHliK1QxVCtaTmphbmFEOXQvcU8xdVozdldHR2ZnSWdaY3g1YWxsOE9DODByek5SUDllSmovYVlESTF2bitJemIxSWRMMUh4K0djPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZWRU5EUVc1VFowRjNTVUpCWjBsSlRUVldRa2hPTW0xeE9EaDNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlRVeVkwZGhXQ3Q2WWpkWUt6RXJXVXR3VXpOdmJFUmlLMEpCZFhkTU1WQkROV3g0Ykc1aVVuaFhXVWcwTVhJemFrNTBlRkFLUlhrM00zZGlSVFZzVW04eGFXcHZWSGxNV0c5NFNXcHJMMFJOV0hvdk9HRXZOazlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR05CVFVWUlEwbEhXR1ZGY3pBdmVuRnFVekpsTVhObVpHc3JlV2xRTXdwV1VGbFdMMDlwYUZZeWVGUTJPRFJPWlN0VmJFRnBRbWN6V2tVMmNucDJlRTlSVDBWU2VITmpSVW8zV2tSMWQwSnZUMjAzVFhBellUbFdVbGxZVERaNkNqUlJQVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICUDADAgEAMIICRwYJKoZIhvcNAQcCoIICODCCAjQCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgIYgyEEu8resegJqeA0JsP+eOcFQtcMrOPW29hWdayFsCFFUKFl6+hAjSIdnvWjbttyWFksqpGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZgwggGUAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIAz2HHBaFnOjQiFhd9OQvE4aaKOH0jZ1IFSzc2g7tRe5MH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRHMEUCIQCArsaM9AgHXd7gn8HO/iqNzOE6BIv13fYfR87i0ajq3wIgbSUmZdxu61nEMqoBJ2nKXlmh+QKVWHG2dLENPN/QO58="}]}},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEUCIQDjUb/AzGSLoSkg0yb+T1T+ZNjanaD9t/qO1uZ3vWGGfgIgZcx5all8OC80rzNRP9eJj/aYDI1vn+Izb1IdL1Hx+Gc="}]}} diff --git a/internal/intoto/testdata/slsa-v0.2-provenance.sigstore.json b/internal/intoto/testdata/slsa-v0.2-provenance.sigstore.json new file mode 100644 index 000000000..6fd21bb90 --- /dev/null +++ b/internal/intoto/testdata/slsa-v0.2-provenance.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInN1YmplY3QiOlt7Im5hbWUiOiJhcnRpZmFjdC5iaW4iLCJkaWdlc3QiOnsic2hhMjU2IjoiZTg4OTUxMjkxYmNhMDgwN2E0MWYzYzAxNDQwMDliZmZmNWM2Yzc3ZGMxMzEwZDUyZThjNmQ2MmZlZWU5NjBhOCJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInByZWRpY2F0ZSI6eyJidWlsZGVyIjp7ImlkIjoiaHR0cHM6Ly9naXRodWIuY29tL3Nsc2EtZnJhbWV3b3JrL3Nsc2EtZ2l0aHViLWdlbmVyYXRvci8uZ2l0aHViL3dvcmtmbG93cy9nZW5lcmF0b3JfZ2VuZXJpY19zbHNhMy55bWxAcmVmcy90YWdzL3YyLjAuMCJ9LCJidWlsZFR5cGUiOiJodHRwczovL2dpdGh1Yi5jb20vc2xzYS1mcmFtZXdvcmsvc2xzYS1naXRodWItZ2VuZXJhdG9yL2dlbmVyaWNAdjEiLCJpbnZvY2F0aW9uIjp7ImNvbmZpZ1NvdXJjZSI6eyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJzaGExIjoiNGY5ZDJhN2MxZTZiOGEzZDVmMGUyYzliN2ExZDRlNmY4YzNiNWE3MCJ9LCJlbnRyeVBvaW50IjoiLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWwifSwiZW52aXJvbm1lbnQiOnsiZ2l0aHViX3JlcG9zaXRvcnlfaWQiOiIxMjM0NTY3ODkiLCJnaXRodWJfcnVuX2lkIjoiMTIzNDU2Nzg5MCIsImdpdGh1Yl9ydW5fYXR0ZW1wdCI6IjEifX0sIm1ldGFkYXRhIjp7ImJ1aWxkSW52b2NhdGlvbklEIjoiaHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0L2FjdGlvbnMvcnVucy8xMjM0NTY3ODkwL2F0dGVtcHRzLzEifSwibWF0ZXJpYWxzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJzaGExIjoiNGY5ZDJhN2MxZTZiOGEzZDVmMGUyYzliN2ExZDRlNmY4YzNiNWE3MCJ9fV19fQ==", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIB+DTYlgZkxxHxpE2ruudIr92YITvhBNOtFk9UjZpZ95AiEAniO+B7P8W5zbF6S+mrJqoeQnSAAdLCw3Ptp7oOwIkO8=" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzjCCAnSgAwIBAgIIM7Dooo0O26AwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETTrx55b+DdBAM/BeJBz0t8pSNU24jsaPnyEO2zQj2O2/c0QwBmqxvJ5GJPCDNtL3vt6QpN1qALIpjjKnOgAlWaOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIEQcFjk8k6N4ZIKyairc2Yd+LdSlMnTzJMNBng7YMARfAiEAgRMsljw/znCNPsJ/oX26tug3c0WJhgfs1nDAx59SJeQ=" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUTADAgEAMIICSAYJKoZIhvcNAQcCoIICOTCCAjUCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgghq7ZbeBPdQxJn/gmoTQoKACBaQE8ePXNT3x3388W1gCFEocrZ+d2/hVlf2DRwzPnvMso7HjGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZkwggGVAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIHB5YUflpcbP5bUmH7ad4HSgzH2o0zpSKDIVQmxCoxeFMH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRIMEYCIQCV8J0a3UamuEwOLdeCRJF5FEP25TKrkR/KpPUJBEjPXAIhAJHhINX+v96LKlC7I/7ysS4l1OBsrCCdl2FZqW0q/5aP" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjQxMGQ5MTNmOGUwMGVlOGYzZTg0MGMyMzM2NDAwYzgzMTVmZWJlNWE2MjQ4MzFkMGRiYzYxNmEyMTQ0ZWMxYWMifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmZjBkYmU2NDA0MmI2ZmU5ZTJjNzYyNTFkMjc3ZWNlYWMwOGNmN2U0YmJjNTgwNGQyYWQxNTg0NjBkZTJjMWFhIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJQitEVFlsZ1preHhIeHBFMnJ1dWRJcjkyWUlUdmhCTk90Rms5VWpacFo5NUFpRUFuaU8rQjdQOFc1emJGNlMrbXJKcW9lUW5TQUFkTEN3M1B0cDdvT3dJa084PSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZha05EUVc1VFowRjNTVUpCWjBsSlRUZEViMjl2TUU4eU5rRjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlZSVWNuZzFOV0lyUkdSQ1FVMHZRbVZLUW5vd2REaHdVMDVWTWpScWMyRlFibmxGVHpKNlVXb3lUekl2WXpCUmQwSnRjWGdLZGtvMVIwcFFRMFJPZEV3emRuUTJVWEJPTVhGQlRFbHdhbXBMYms5blFXeFhZVTlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbEZVV05HYW1zNGF6Wk9ORnBKUzNsaGFYSmpNbGxrS3dwTVpGTnNUVzVVZWtwTlRrSnVaemRaVFVGU1prRnBSVUZuVWsxemJHcDNMM3B1UTA1UWMwb3ZiMWd5Tm5SMVp6TmpNRmRLYUdkbWN6RnVSRUY0TlRsVENrcGxVVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEUCIA5oLO+cymATJPoGIHNA1z3SRnrHbDyJ2Zz4Uha8kaIPAiEAhlJ2wfapfc3SsHd/adA6qn2UHDpmoksWckj4cFOkk3g=" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nQb7D83739OPtdRqBkJnxNEj9idloS+6iQ/k8q4ooUnI=\n\n— rekor.localhost nk5pvzBGAiEAstME1WOFKDR+wJx5rqZMyOqo1zjkf0bH9+2fur5KudoCIQDMNfMNYlOCB3/RhEuQltbQRceVcSrescuQFs1lqNmWqw==\n" + }, + "rootHash": "Qb7D83739OPtdRqBkJnxNEj9idloS+6iQ/k8q4ooUnI=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/spdx-sbom.sigstore.json b/internal/intoto/testdata/spdx-sbom.sigstore.json new file mode 100644 index 000000000..be0e5ee1c --- /dev/null +++ b/internal/intoto/testdata/spdx-sbom.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zcGR4LmRldi9Eb2N1bWVudC92Mi4zIiwicHJlZGljYXRlIjp7InNwZHhWZXJzaW9uIjoiU1BEWC0yLjMiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJTUERYSUQiOiJTUERYUmVmLURPQ1VNRU5UIiwibmFtZSI6ImFydGlmYWN0LmJpbiIsImRvY3VtZW50TmFtZXNwYWNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGR4L2FydGlmYWN0LmJpbi0xIiwiY3JlYXRpb25JbmZvIjp7ImNyZWF0ZWQiOiIyMDI2LTExLTAxVDA5OjAwOjAwWiIsImNyZWF0b3JzIjpbIlRvb2w6IGV4YW1wbGUtc2JvbS10b29sLTEuMCJdfSwicGFja2FnZXMiOlt7Im5hbWUiOiJhcnRpZmFjdC5iaW4iLCJTUERYSUQiOiJTUERYUmVmLVBhY2thZ2UtYXJ0aWZhY3QiLCJ2ZXJzaW9uSW5mbyI6IjEuMi4zIiwiZG93bmxvYWRMb2NhdGlvbiI6Ik5PQVNTRVJUSU9OIn1dLCJyZWxhdGlvbnNoaXBzIjpbeyJzcGR4RWxlbWVudElkIjoiU1BEWFJlZi1ET0NVTUVOVCIsInJlbGF0aW9uc2hpcFR5cGUiOiJERVNDUklCRVMiLCJyZWxhdGVkU3BkeEVsZW1lbnQiOiJTUERYUmVmLVBhY2thZ2UtYXJ0aWZhY3QifV19fQ==", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEYCIQDHg5zQdU+vyLCNO/CkRewrV28jxeha+GmthSJooucruAIhAJvunhciWhrbrD+QRnoJ7FbPFF8A07pKNRrLE9q7urpA" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzjCCAnSgAwIBAgIIHXHMoQTezR4wCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEK6Qvf5nCgBwQNni3L9mrXQDId1z1O4expTWsq6cOHAeokVHMlg7bB0us/x3Dwk0ZVD72eyPYpcKUqGPcI/IzN6OCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIQD+HbjIHznttt0RLeegIaLs5kDMc9qZ9P6R+yzkGhqfWQIgVKfjaIMpRxo5ebVMUpyfQyTHkrN92xhlSm59rB6M56Y=" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUDADAgEAMIICRwYJKoZIhvcNAQcCoIICODCCAjQCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgFDSBSUMm1rieNuqhoEl0XtyRagcayjEvm47cWh5DYSICFC7BnmwP3xsQEI2c3KTeX4eG5MNEGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZgwggGUAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIHe02X47RBm2fYsu4kaJmCmzh4Vr209qPYXTKTD1zY+VMH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRHMEUCIQCrj6hAqAHNC3UN+lh7fCw7wphJV7yz3u69MPCrbjTpgAIgNAtfHDXPQ5dVxzkmG/1LqdHxmxz2CKv1WrEf8ozgxeI=" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjcxNjFiOTY3YmQ4MDM4NmNlZjIyMzM3ZjMyMzk4ZjZkYjNjYzk1ZmJmNmUwMTM2NjNkYWE3Y2Y1NmZhYTcxMTEifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIxMTEwZDQ2MGFjMmIyMzRjMTRmZThlYWM1NTRhOGQ5NDcyYTYxZWE5YjQ3YTY0N2M3MWUxMThlY2ZjYjE2YjYxIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FWUNJUURIZzV6UWRVK3Z5TENOTy9Da1Jld3JWMjhqeGVoYStHbXRoU0pvb3VjcnVBSWhBSnZ1bmhjaVdocmJyRCtRUm5vSjdGYlBGRjhBMDdwS05SckxFOXE3dXJwQSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZha05EUVc1VFowRjNTVUpCWjBsSlNGaElUVzlSVkdWNlVqUjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlVzMlVYWm1OVzVEWjBKM1VVNXVhVE5NT1cxeVdGRkVTV1F4ZWpGUE5HVjRjRlJYYzNFMlkwOUlRV1Z2YTFaSVRXeG5OMklLUWpCMWN5OTRNMFIzYXpCYVZrUTNNbVY1VUZsd1kwdFZjVWRRWTBrdlNYcE9OazlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbFJSQ3RJWW1wSlNIcHVkSFIwTUZKTVpXVm5TV0ZNY3dvMWEwUk5Zemx4V2psUU5sSXJlWHByUjJoeFpsZFJTV2RXUzJacVlVbE5jRko0YnpWbFlsWk5WWEI1WmxGNVZFaHJjazQ1TW5ob2JGTnROVGx5UWpaTkNqVTJXVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIFjKz6kSNVM15OwZUrgYIuEmtKy6lcTbdMJJPMuvm+b+AiBVSfSsNzrxoqBbF/c0pVn8APjAbfxHVVAi6C/TmRh13Q==" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nCpkccvDMU4zT833KE3alNlZKQjkPJfHMCn7cDF65wco=\n\n— rekor.localhost nk5pvzBGAiEAz1OlyTd/H2s4QDfBr9k7HfNbcqaARGzLoSOyLoDXUh8CIQDLdg8XYbpf++rVC67pXbCiNrnINyMrzRJ9hn+J55MC2Q==\n" + }, + "rootHash": "CpkccvDMU4zT833KE3alNlZKQjkPJfHMCn7cDF65wco=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/statements/buildkit-provenance.json b/internal/intoto/testdata/statements/buildkit-provenance.json new file mode 100644 index 000000000..82dbe6d87 --- /dev/null +++ b/internal/intoto/testdata/statements/buildkit-provenance.json @@ -0,0 +1,37 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": "pkg:docker/acme/widget@1.2.3?platform=linux%2Famd64", "digest": {"sha256": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}} + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md", + "externalParameters": { + "configSource": { + "uri": "https://github.com/acme/widget.git#refs/heads/main", + "digest": {"sha1": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}, + "path": "Dockerfile" + }, + "request": {"frontend": "dockerfile.v0", "args": {}} + }, + "internalParameters": {"builderPlatform": "linux/amd64"}, + "resolvedDependencies": [ + {"uri": "pkg:docker/alpine@3.20?platform=linux%2Famd64", "digest": {"sha256": "0f2d5c38dd7a4f4f733e688e3a6733cb5ab1ac6e3cb4603a5dd564e5bfb80eed"}}, + {"uri": "https://github.com/acme/widget.git#refs/heads/main", "digest": {"sha1": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}} + ] + }, + "runDetails": { + "builder": {"id": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1"}, + "metadata": { + "invocationID": "rpv7a389uzil5lqmrgwhijwjz", + "buildkit_metadata": { + "vcs": { + "source": "https://github.com/acme/widget", + "revision": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70" + } + } + } + } + } +} diff --git a/internal/intoto/testdata/statements/cyclonedx-sbom.json b/internal/intoto/testdata/statements/cyclonedx-sbom.json new file mode 100644 index 000000000..eb7dabc7d --- /dev/null +++ b/internal/intoto/testdata/statements/cyclonedx-sbom.json @@ -0,0 +1,20 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": "artifact.bin", "digest": {"sha256": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}} + ], + "predicateType": "https://cyclonedx.org/bom", + "predicate": { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "timestamp": "2026-11-01T09:00:00Z", + "tools": {"components": [{"type": "application", "name": "example-sbom-tool", "version": "1.0"}]}, + "component": {"type": "application", "name": "artifact.bin", "version": "1.2.3"} + }, + "components": [ + {"type": "library", "name": "left-pad", "version": "1.3.0", "purl": "pkg:npm/left-pad@1.3.0"} + ] + } +} diff --git a/internal/intoto/testdata/statements/github-provenance.json b/internal/intoto/testdata/statements/github-provenance.json new file mode 100644 index 000000000..ade73fec2 --- /dev/null +++ b/internal/intoto/testdata/statements/github-provenance.json @@ -0,0 +1,34 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": "artifact.bin", "digest": {"sha256": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}} + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://actions.github.io/buildtypes/workflow/v1", + "externalParameters": { + "workflow": { + "ref": "refs/heads/main", + "repository": "https://github.com/acme/widget", + "path": ".github/workflows/build.yml" + } + }, + "internalParameters": { + "github": { + "event_name": "push", + "repository_id": "123456789", + "repository_owner_id": "987654321", + "runner_environment": "github-hosted" + } + }, + "resolvedDependencies": [ + {"uri": "git+https://github.com/acme/widget@refs/heads/main", "digest": {"gitCommit": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}} + ] + }, + "runDetails": { + "builder": {"id": "https://github.com/actions/runner/github-hosted"}, + "metadata": {"invocationId": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1"} + } + } +} diff --git a/internal/intoto/testdata/statements/slsa-v0.2-provenance.json b/internal/intoto/testdata/statements/slsa-v0.2-provenance.json new file mode 100644 index 000000000..af03bc678 --- /dev/null +++ b/internal/intoto/testdata/statements/slsa-v0.2-provenance.json @@ -0,0 +1,29 @@ +{ + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [ + {"name": "artifact.bin", "digest": {"sha256": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}} + ], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "builder": {"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0"}, + "buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@v1", + "invocation": { + "configSource": { + "uri": "git+https://github.com/acme/widget@refs/heads/main", + "digest": {"sha1": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}, + "entryPoint": ".github/workflows/release.yml" + }, + "environment": { + "github_repository_id": "123456789", + "github_run_id": "1234567890", + "github_run_attempt": "1" + } + }, + "metadata": { + "buildInvocationID": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1" + }, + "materials": [ + {"uri": "git+https://github.com/acme/widget@refs/heads/main", "digest": {"sha1": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}} + ] + } +} diff --git a/internal/intoto/testdata/statements/spdx-sbom.json b/internal/intoto/testdata/statements/spdx-sbom.json new file mode 100644 index 000000000..fb6db2034 --- /dev/null +++ b/internal/intoto/testdata/statements/spdx-sbom.json @@ -0,0 +1,29 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": "artifact.bin", "digest": {"sha256": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}} + ], + "predicateType": "https://spdx.dev/Document/v2.3", + "predicate": { + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "artifact.bin", + "documentNamespace": "https://example.com/spdx/artifact.bin-1", + "creationInfo": { + "created": "2026-11-01T09:00:00Z", + "creators": ["Tool: example-sbom-tool-1.0"] + }, + "packages": [ + { + "name": "artifact.bin", + "SPDXID": "SPDXRef-Package-artifact", + "versionInfo": "1.2.3", + "downloadLocation": "NOASSERTION" + } + ], + "relationships": [ + {"spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-Package-artifact"} + ] + } +} diff --git a/internal/intoto/testdata/statements/vuln-scan.json b/internal/intoto/testdata/statements/vuln-scan.json new file mode 100644 index 000000000..1ef15daec --- /dev/null +++ b/internal/intoto/testdata/statements/vuln-scan.json @@ -0,0 +1,12 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": "artifact.bin", "digest": {"sha256": "e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}} + ], + "predicateType": "https://cosign.sigstore.dev/attestation/vuln/v1", + "predicate": { + "invocation": {"uri": "https://github.com/acme/widget/actions/runs/1234567890"}, + "scanner": {"uri": "pkg:github/aquasecurity/trivy@0.50.0", "version": "0.50.0", "result": {}}, + "metadata": {"scanStartedOn": "2026-11-01T09:00:00Z", "scanFinishedOn": "2026-11-01T09:01:00Z"} + } +} diff --git a/internal/intoto/testdata/statements/wrong-subject-provenance.json b/internal/intoto/testdata/statements/wrong-subject-provenance.json new file mode 100644 index 000000000..1df841f91 --- /dev/null +++ b/internal/intoto/testdata/statements/wrong-subject-provenance.json @@ -0,0 +1,34 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + {"name": "other.bin", "digest": {"sha256": "11de96226896b0fcf4ea66a11a63a5fd9f4e211fe94b62b7eebf4485c817b2d8"}} + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://actions.github.io/buildtypes/workflow/v1", + "externalParameters": { + "workflow": { + "ref": "refs/heads/main", + "repository": "https://github.com/acme/widget", + "path": ".github/workflows/build.yml" + } + }, + "internalParameters": { + "github": { + "event_name": "push", + "repository_id": "123456789", + "repository_owner_id": "987654321", + "runner_environment": "github-hosted" + } + }, + "resolvedDependencies": [ + {"uri": "git+https://github.com/acme/widget@refs/heads/main", "digest": {"gitCommit": "4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}} + ] + }, + "runDetails": { + "builder": {"id": "https://github.com/actions/runner/github-hosted"}, + "metadata": {"invocationId": "https://github.com/acme/widget/actions/runs/1234567890/attempts/1"} + } + } +} diff --git a/internal/intoto/testdata/tampered-provenance.sigstore.json b/internal/intoto/testdata/tampered-provenance.sigstore.json new file mode 100644 index 000000000..e5c223cae --- /dev/null +++ b/internal/intoto/testdata/tampered-provenance.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEYCIQC6Da99KZYnz8IbkAHzIeaKZm59IaoJsyDjgs3jKWLtUwIhAMqZrnFxbRSQm0MN4A5nybmFiGGSotN7AGeb0Bh2nVEN" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzzCCAnSgAwIBAgIIHkCF8v12eKkwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtXcyncIzoRirDUq6HRqXrm4mlNDAWJKh8+WcuHdGS8/35sA7hNyNGsHE1994meki+nHMo8W4xt6EqEQ0T3y4qqOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0kAMEYCIQDKIKiSzzSctJq/8RSEb6HUkM7G1JxN1b8eBNfBr5E2FgIhAMsrcBQcP+T2P7KjtY/lIS1i6Ef2rBsAn4zBU0HSRmdD" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUDADAgEAMIICRwYJKoZIhvcNAQcCoIICODCCAjQCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgFU86FD0iLTqORp6i2wHBzgyfNl/6kx/n0SfqxGaHOyQCFAm/2n1qQUwE3VYmtXOB9YDIDuATGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZgwggGUAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIIoegq9W0Nx4KC7jEgIsL2NlmxhlxA8npiSDtVizzav1MH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRHMEUCIDCyd3ypyt5vg2PZx43GZ2VECxU40A3I2DKMSjTFYScpAiEAwbnVnQuaZT8gqQT2zrNwx7V95+Ahn5n0/ThsZyHbvAo=" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjA1NmIxZWRhMzIzZTJjZDhjNDRhOWRmYjZmZjdkZjQ3MGE3ZmJkMmI5ZjM2YzVhMTcyYjVkZWRjYmE3YTM1N2MifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FWUNJUUM2RGE5OUtaWW56OElia0FIekllYUtabTU5SWFvSnN5RGpnczNqS1dMdFV3SWhBTXFacm5GeGJSU1FtME1ONEE1bnlibUZpR0dTb3RON0FHZWIwQmgyblZFTiIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZla05EUVc1VFowRjNTVUpCWjBsSlNHdERSamgyTVRKbFMydDNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlhSWVkzbHVZMGw2YjFKcGNrUlZjVFpJVW5GWWNtMDBiV3hPUkVGWFNrdG9PQ3RYWTNWSVpFZFRPQzh6TlhOQk4yaE9lVTRLUjNOSVJURTVPVFJ0Wld0cEsyNUlUVzg0VnpSNGREWkZjVVZSTUZRemVUUnhjVTlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR3RCVFVWWlEwbFJSRXRKUzJsVGVucFRZM1JLY1M4NFVsTkZZalpJVlFwclRUZEhNVXA0VGpGaU9HVkNUbVpDY2pWRk1rWm5TV2hCVFhOeVkwSlJZMUFyVkRKUU4wdHFkRmt2YkVsVE1XazJSV1l5Y2tKelFXNDBla0pWTUVoVENsSnRaRVFLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIHuZn4IJzo5rPYhFN40J5mwzj55Cto5rhJmumH6FAASPAiBnkUG0HUbW8+T/eu7QHi16bSh+NA2y2fRxe7nKUU2ZYg==" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nTuoiXtOP3009QjM7fsVCOUiUgbEpA02LIN0v6wagBlg=\n\n— rekor.localhost nk5pvzBFAiBvIXVQaB7ssmHPoCKSPotG559XhOVbODCt4T7TMMcmkgIhAOM+I147Pwj0RhHxFoQapIptWtfhepu2NvSRBzvF54kl\n" + }, + "rootHash": "TuoiXtOP3009QjM7fsVCOUiUgbEpA02LIN0v6wagBlg=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/trusted_root.json b/internal/intoto/testdata/trusted_root.json new file mode 100644 index 000000000..e71a047d2 --- /dev/null +++ b/internal/intoto/testdata/trusted_root.json @@ -0,0 +1,85 @@ +{ + "certificateAuthorities": [ + { + "certChain": { + "certificates": [ + { + "rawBytes": "MIIBxzCCAW6gAwIBAgIBATAKBggqhkjOPQQDAjAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTI2MTExMTExMDg1MloXDTI2MTExMTEzMTA1MlowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQFS2j3glGf1brMSfbdabRMqV+D5ZXHEL+RmywjgWo3VKuZ01VUJ4jSNT5Oks77iMcxkeWpzEJyuA/ba3FvPNRuo3gwdjAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUbTkXZ+y/ail+YqU5LxeR7pwQQjowHwYDVR0jBBgwFoAUJa2SJrtjc1Q2XflTI6ju+ZZhkxwwCgYIKoZIzj0EAwIDRwAwRAIgRZtC2Rpw5CvTC4j8+ocH4rC3ZTP575jIzWm0STD1Wi8CIEiPPOKveuy8FccA9UizACl7BdYSlPEVugD+o19Ld5Qm" + }, + { + "rawBytes": "MIIBhDCCASugAwIBAgIBATAKBggqhkjOPQQDAjAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTI2MTExMTA2MTA1MloXDTI2MTExMTE2MTA1MlowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAdIUThOUqxdDLDMBcyxSM8ef/9466bqu3Stp26F/3eGzOO6QjygUiGSLoN4MMAHLuoy8BXtSl9UhRrSJpXCVzCjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQlrZImu2NzVDZd+VMjqO75lmGTHDAKBggqhkjOPQQDAgNHADBEAiB0AvcQowg+2XtLXdU/ppCrGUmfClV52a0xNdCwJa39fwIgRN3/Y8GXB6HoI7CbnMB95DdoqKxrrfJKQkzXG9YJKJw=" + } + ] + }, + "subject": { + "commonName": "sigstore", + "organization": "sigstore.dev" + }, + "uri": "https://fulcio.example.test", + "validFor": { + "start": "2026-11-11T10:10:52.852908Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "test", + "hashAlgorithm": "SHA2_256", + "logId": { + "keyId": "5yiE1mk7eI+XDPJS/p2GRCcqx3DGEcA9JJhKgbGQ+kw=" + }, + "publicKey": { + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEK7VbOGuADkSvHfPvgOEMf509m/WgB8w5Xt965zSPIOxrXEQ9i/3QQvZB+zIClDVK8UqkYu5HmI/9xztsiL2qkg==", + "validFor": { + "end": "2026-11-11T12:10:52.852912Z", + "start": "2026-11-11T10:10:52.852912Z" + } + } + } + ], + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "timestampAuthorities": [ + { + "certChain": { + "certificates": [ + { + "rawBytes": "MIIBdDCCARugAwIBAgIBATAKBggqhkjOPQQDAjA7MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIjAgBgNVBAMTGXNpZ3N0b3JlLXRzYS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwNTUyWhcNMjYxMTExMTExNTUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8yOiSymZZkF3OMJRxpnugdk5u2UfLaa0sktuqzfvwHF/w7B+t7s0hrhT1B/68c3hNM6/Ok7Ti3PCbedGrfZJnqNLMEkwDgYDVR0PAQH/BAQDAgeAMB8GA1UdIwQYMBaAFP4nIpGXKOIaG9cWjuooXPJcKu5aMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMCA0cAMEQCIFMoYu13eE0BSnBXREMxe+3Dd30vem8YiZVFDVEY89ETAiAem9OjMN/uShTK0MTa3r48WPWMtKKeRIwuXDzSCubGAA==" + }, + { + "rawBytes": "MIIBzTCCAXKgAwIBAgIBATAKBggqhkjOPQQDAjAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTI2MTExMTExMDg1MloXDTI2MTExMTEzMTA1MlowOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkMlaYjsK0c6PWJhSZHOOgm5k7agnBxoAHvtrqgMxpq2Ha833SET8NMtoB9dxl/IWZB4+h8KodDEbX4ICEGTb76N4MHYwDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP4nIpGXKOIaG9cWjuooXPJcKu5aMB8GA1UdIwQYMBaAFEqH5767VQ0rHMDU6eMCzp/fSrjJMAoGCCqGSM49BAMCA0kAMEYCIQDlEsPPMcZU0ohB0myQWfNVhiPbTVJj+S+VGSG3vjxBoQIhANE0Xy3I3W9U00dYBxVPGhhx7mZ0QBTVZTMrmPt8Tz/Q" + }, + { + "rawBytes": "MIIBhDCCASugAwIBAgIBATAKBggqhkjOPQQDAjAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTI2MTExMTA2MTA1MloXDTI2MTExMTE2MTA1MlowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMWe3g2ENzrQJVADiA08KmI19wenoQNRiXK7zUYrtA0lB/bPodpx1D0MMODt1flkZuPYnUOrClMM6p8NaGSRtMmjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRKh+e+u1UNKxzA1OnjAs6f30q4yTAKBggqhkjOPQQDAgNHADBEAiA0HCjtiXEcGTUl5ES8W67/ZAIeKxGNOw7ZLPzL1tSmLQIgaJ6GyUMXKGgbIhuWx4YkhQ+0k/gxTUGMVEd3ECE+2zo=" + } + ] + }, + "subject": { + "commonName": "sigstore", + "organization": "sigstore.dev" + }, + "uri": "https://virtual.tsa.sigstore.dev", + "validFor": { + "end": "2026-11-11T12:10:52.852468Z", + "start": "2026-11-11T06:10:52.852468Z" + } + } + ], + "tlogs": [ + { + "baseUrl": "https://rekor.localhost", + "hashAlgorithm": "SHA2_256", + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + }, + "publicKey": { + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf/F9lsZvwjXqlh/nbKbtnXMs25xHJ9+qPIEqcyBhMxvgc5Izhpx59esTN5qRVFsRG/ySiDduURvZXog60/uakA==", + "validFor": { + "end": "2026-11-11T12:10:52.852916Z", + "start": "2026-11-11T10:10:52.852916Z" + } + } + } + ] +} diff --git a/internal/intoto/testdata/unsigned-spdx-sbom.dsse.json b/internal/intoto/testdata/unsigned-spdx-sbom.dsse.json new file mode 100644 index 000000000..ecd13cda2 --- /dev/null +++ b/internal/intoto/testdata/unsigned-spdx-sbom.dsse.json @@ -0,0 +1,5 @@ +{ + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zcGR4LmRldi9Eb2N1bWVudC92Mi4zIiwicHJlZGljYXRlIjp7InNwZHhWZXJzaW9uIjoiU1BEWC0yLjMiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJTUERYSUQiOiJTUERYUmVmLURPQ1VNRU5UIiwibmFtZSI6ImFydGlmYWN0LmJpbiIsImRvY3VtZW50TmFtZXNwYWNlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9zcGR4L2FydGlmYWN0LmJpbi0xIiwiY3JlYXRpb25JbmZvIjp7ImNyZWF0ZWQiOiIyMDI2LTExLTAxVDA5OjAwOjAwWiIsImNyZWF0b3JzIjpbIlRvb2w6IGV4YW1wbGUtc2JvbS10b29sLTEuMCJdfSwicGFja2FnZXMiOlt7Im5hbWUiOiJhcnRpZmFjdC5iaW4iLCJTUERYSUQiOiJTUERYUmVmLVBhY2thZ2UtYXJ0aWZhY3QiLCJ2ZXJzaW9uSW5mbyI6IjEuMi4zIiwiZG93bmxvYWRMb2NhdGlvbiI6Ik5PQVNTRVJUSU9OIn1dLCJyZWxhdGlvbnNoaXBzIjpbeyJzcGR4RWxlbWVudElkIjoiU1BEWFJlZi1ET0NVTUVOVCIsInJlbGF0aW9uc2hpcFR5cGUiOiJERVNDUklCRVMiLCJyZWxhdGVkU3BkeEVsZW1lbnQiOiJTUERYUmVmLVBhY2thZ2UtYXJ0aWZhY3QifV19fQ==", + "payloadType": "application/vnd.in-toto+json", + "signatures": [] +} diff --git a/internal/intoto/testdata/vuln-only.intoto.jsonl b/internal/intoto/testdata/vuln-only.intoto.jsonl new file mode 100644 index 000000000..2c6f86b87 --- /dev/null +++ b/internal/intoto/testdata/vuln-only.intoto.jsonl @@ -0,0 +1,2 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIICzTCCAnSgAwIBAgIIGKCWcPVZqlkwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaTKzMthzDYqvbHRUnehpqQULrcdP0s0vv2rjfAuJ5kmwrc1TVTBTbPll2ycI46A17x8v4CfA1roZl89mfE84OqOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0cAMEQCIDThgYMK0IauCFf3VEBGGQeGYkb5nCwEVzNd0x6QKjsCAiB86d0oGtqzdUUdqPWCNxdGw1ME+1QbmX+aXtJt8nUtgg=="},"tlogEntries":[{"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1794395512","inclusionPromise":{"signedEntryTimestamp":"MEQCIEjxspxccn6s1e8kNVNZlQpIVTkrLxbbaXRDpY5C9dwiAiBADlahdJ9zIqGw5O1z6m3lvEwRkpznQW1EXQAx7Lkj+w=="},"inclusionProof":{"rootHash":"N74nxzp6juDs4ZS6m0LWlwxP3CKBM70/R83kEtNs54E=","treeSize":"1","checkpoint":{"envelope":"rekor.localhost - 123\n42\nN74nxzp6juDs4ZS6m0LWlwxP3CKBM70/R83kEtNs54E=\n\n— rekor.localhost nk5pvzBEAiBQE7mpAL3YG5ce10CW3987xBuRL3tEUsElvBsLHb+MGgIgKBU4zS4siqK6z8aLZI/5B8DlU4rMNvHMkMHIbnRjWlE=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjMxNDI1NzQwM2U2YWVhM2E4NmZiZGFhODU3OTc2ODI3OTZhZDk1YTkzODk2YWQ4YWQ3MzcwNjk5Y2QwNGNhNGIifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJiM2Q0NTk3OWYwMTRjNTAwNDdmOTMyZWM4NWYxYzFlZGQ1YjY4YWRiMmRiYTMwMzUxMDYzNTFjMzZjNWRhNmZmIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJQkxDK2JNZnU3Q3lWaUdKNnFQTm9jTDZOMk1DS3o5Q0dRaVFiSVBHck5jS0FpRUErVkp5Y3NSQVY2ZGZPdC9LdzVlbGRJcDJaczVDd0xtOFcrRFlCaEp2bFBBPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZWRU5EUVc1VFowRjNTVUpCWjBsSlIwdERWMk5RVmxweGJHdDNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUldGVVMzcE5kR2g2UkZseGRtSklVbFZ1Wldod2NWRlZUSEpqWkZBd2N6QjJkakp5YW1aQmRVbzFhMjEzY21NeFZGWlVRbFFLWWxCc2JESjVZMGswTmtFeE4zZzRkalJEWmtFeGNtOWFiRGc1YldaRk9EUlBjVTlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR05CVFVWUlEwbEVWR2huV1UxTE1FbGhkVU5HWmpOV1JVSkhSMUZsUndwWmEySTFia04zUlZaNlRtUXdlRFpSUzJwelEwRnBRamcyWkRCdlIzUnhlbVJWVldSeFVGZERUbmhrUjNjeFRVVXJNVkZpYlZncllWaDBTblE0YmxWMENtZG5QVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ=="}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICTzADAgEAMIICRgYJKoZIhvcNAQcCoIICNzCCAjMCAQMxDTALBglghkgBZQMEAgEwgYEGCyqGSIb3DQEJEAEEoHIEcDBuAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgi4cpOgY0FLfrzhRiSQw6camGvlh2rVSL1U0AeEJxIDYCFF1wD4P0V0G0izc32/2qPXPzy2bmGA8yMDI2MTExMTExMTA1MlqgBKQCMACgADGCAZcwggGTAgEBMEAwOzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSIwIAYDVQQDExlzaWdzdG9yZS10c2EtaW50ZXJtZWRpYXRlAgEBMAsGCWCGSAFlAwQCAaCB6jAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MTExMTExMTA1MlowLwYJKoZIhvcNAQkEMSIEIDBtQtx3LILeHz6oiQHasWHfI1fu1+rFeUQK8nnOMJHBMH0GCyqGSIb3DQEJEAIvMW4wbDBqMGgEIFSqjKR8dRclNiy8UAds7vX8qb7VgJJPIykOgm47QXPmMEQwP6Q9MDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATAKBggqhkjOPQQDAgRGMEQCIHDA0OkYQSMIVkbMqEv62xLLurj6N/7bFvZBVSo9AgG8AiBUIHUEqlPadf2Gem5uP20QeAATm9E0tJYA71vQoZkeOA=="}]}},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9jb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3Z1bG4vdjEiLCJwcmVkaWNhdGUiOnsiaW52b2NhdGlvbiI6eyJ1cmkiOiJodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvYWN0aW9ucy9ydW5zLzEyMzQ1Njc4OTAifSwic2Nhbm5lciI6eyJ1cmkiOiJwa2c6Z2l0aHViL2FxdWFzZWN1cml0eS90cml2eUAwLjUwLjAiLCJ2ZXJzaW9uIjoiMC41MC4wIiwicmVzdWx0Ijp7fX0sIm1ldGFkYXRhIjp7InNjYW5TdGFydGVkT24iOiIyMDI2LTExLTAxVDA5OjAwOjAwWiIsInNjYW5GaW5pc2hlZE9uIjoiMjAyNi0xMS0wMVQwOTowMTowMFoifX19","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEUCIBLC+bMfu7CyViGJ6qPNocL6N2MCKz9CGQiQbIPGrNcKAiEA+VJycsRAV6dfOt/Kw5eldIp2Zs5CwLm8W+DYBhJvlPA="}]}} +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"artifact.bin","digest":{"sha256":"e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}}],"predicateType":"https://cosign.sigstore.dev/attestation/vuln/v1","predicate":{"invocation":{"uri":"https://github.com/acme/widget/actions/runs/1234567890"},"scanner":{"uri":"pkg:github/aquasecurity/trivy@0.50.0","version":"0.50.0","result":{}},"metadata":{"scanStartedOn":"2026-11-01T09:00:00Z","scanFinishedOn":"2026-11-01T09:01:00Z"}}} diff --git a/internal/intoto/testdata/wrong-signer-and-unsigned.intoto.jsonl b/internal/intoto/testdata/wrong-signer-and-unsigned.intoto.jsonl new file mode 100644 index 000000000..c8b8f02a1 --- /dev/null +++ b/internal/intoto/testdata/wrong-signer-and-unsigned.intoto.jsonl @@ -0,0 +1,2 @@ +{"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEUCIGb6TKl2yC1WpnZFEA+AYdCgZcNz0PlqfAaacUthZKcCAiEAyHBuRSaM4Onk6q90CN6uljCfZMqV9SevpG09e3cMEQo="}]},"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIIC0TCCAnegAwIBAgIIMQSpmjl3aZAwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYvSuDN9eoiuol8ZHd9MMB37Lg1tvde/R7ue+fBmIf1UAx1rT6mtOUFb0kRgQ65W2KdYTXEyuc8UHjaNxwnKv7aOCAaIwggGeMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBbBgNVHREBAf8EUTBPhk1odHRwczovL2dpdGh1Yi5jb20vbWFsbG9yeS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIQDucC9SsJ3E1IewVYsDMbN8K3mlQ5SfXF8nKFFgFJSkWAIgOG7tDU9ZJ1u9+4TuFWxh3iIn8E7ZEcKdVHtzYkdV2Hs="},"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICUjADAgEAMIICSQYJKoZIhvcNAQcCoIICOjCCAjYCAQMxDTALBglghkgBZQMEAgEwgYIGCyqGSIb3DQEJEAEEoHMEcTBvAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgYEU6P1C2l+a8swnV78QvCS/CC9p9ez6jMaxcRtDuunoCFQDggF8UZ0oR9BHqH5CIX6Vo/8l+vxgPMjAyNjExMTExMTEwNTJaoASkAjAAoAAxggGZMIIBlQIBATBAMDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATALBglghkgBZQMEAgGggeowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjExMTExMTEwNTJaMC8GCSqGSIb3DQEJBDEiBCBBlqXrb50krYwwyMbz3YUznqv33fe6PB/sTyV5Dsg7MTB9BgsqhkiG9w0BCRACLzFuMGwwajBoBCBUqoykfHUXJTYsvFAHbO71/Km+1YCSTyMpDoJuO0Fz5jBEMD+kPTA7MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIjAgBgNVBAMTGXNpZ3N0b3JlLXRzYS1pbnRlcm1lZGlhdGUCAQEwCgYIKoZIzj0EAwIESDBGAiEAnuvYeDoV5Kxk3YTt8yIRIF+5hEJ385D4Ytcy6duf86ECIQDi4S5U2Ge3hJy02Pp0IB4r3cV9TwQGa3Mw/dpg/C9tzA=="}]},"tlogEntries":[{"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjMzNjIyMzkyNzg4NmExNDg5MGI1OGEwNzkzYTQ2ZDI5MTJjODFmOWNjYjE4ZmFiMmVkOTc2MGI2ZjA5NmY5Y2EifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJR2I2VEtsMnlDMVdwblpGRUErQVlkQ2daY056MFBscWZBYWFjVXRoWktjQ0FpRUF5SEJ1UlNhTTRPbms2cTkwQ042dWxqQ2ZaTXFWOVNldnBHMDllM2NNRVFvPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXdWRU5EUVc1bFowRjNTVUpCWjBsSlRWRlRjRzFxYkROaFdrRjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlZsMlUzVkVUamxsYjJsMWIydzRXa2hrT1UxTlFqTTNUR2N4ZEhaa1pTOVNOM1ZsSzJaQ2JVbG1NVlZCZURGeVZEWnRkRThLVlVaaU1HdFNaMUUyTlZjeVMyUlpWRmhGZVhWak9GVklhbUZPZUhkdVMzWTNZVTlEUVdGSmQyZG5SMlZOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENZa0puVGxaSVVrVkNRV1k0UlZWVVFsQm9hekZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZbGRHYzJKSE9Ya0taVk01TTJGWFVtNWFXRkYyVEcxa2NHUkhhREZaYVRrellqTktjbHB0ZUhaa00wMTJXVzVXY0dKSFVYVmxWekZ6VVVoS2JGcHVUWFpoUjFab1draE5kZ3BpVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQmhTRlpwQ21SWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVUnpSME5wYzBkQlVWRkNaemM0ZDBGUlowVk1VWGR5WVVoU01HTklUVFpNZVRrd1lqSjBiR0pwTldnS1dUTlNjR0l5TlhwTWJXUndaRWRvTVZsdVZucGFXRXBxWWpJMU1GcFhOVEJNYlU1MllsUkJkVUpuYjNKQ1owVkZRVmxQTDAxQlJVMUNRMEZOU0cxb01BcGtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1doWk1qRnNURE5rY0ZwSFpHeGtSRUpTUW1kdmNrSm5SVVZCV1U4dlRVRkZWa0pGVFUxUlYyZ3dDbVJJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWFGa3lNV3hNTTJSd1drZGtiR1JET1doWk0xSndZakkxZWt3elNqRmliazEyVFZSSmVrNUVWVElLVG5wbk5VMURPV2hrU0ZKc1lsaENNR041T0hoTlFXOUhRME54UjFOTk5EbENRVTFEUVRCblFVMUZWVU5KVVVSMVkwTTVVM05LTTBVeFNXVjNWbGx6UkFwTllrNDRTek50YkZFMVUyWllSamh1UzBaR1owWktVMnRYUVVsblQwYzNkRVJWT1ZwS01YVTVLelJVZFVaWGVHZ3phVWx1T0VVM1drVmpTMlJXU0hSNkNsbHJaRll5U0hNOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9XX0sImtpbmQiOiJkc3NlIn0=","inclusionPromise":{"signedEntryTimestamp":"MEQCICvvYGJwpdXELUupODYFnNBJgaSSOJLwYfcrZlCvVvDwAiBRYw7nv8rcBXI3t1uLN3WicmVv1p/TKtV0iYcpPApwPA=="},"inclusionProof":{"checkpoint":{"envelope":"rekor.localhost - 123\n42\nYvYM+cfyHLVPs13HIGJ6EMlXI3hALK6qM4lgjg0YHnI=\n\n— rekor.localhost nk5pvzBGAiEA0k56ZATpQCRx+qGVegSeSy6dTrDztBzqrOqOQk96f5ECIQCw+I9Mtquhjcg+5+ejdxgHS7DuISZy/0qT9nUOH98LdQ==\n"},"rootHash":"YvYM+cfyHLVPs13HIGJ6EMlXI3hALK6qM4lgjg0YHnI=","treeSize":"1"},"integratedTime":"1794395512","kindVersion":{"kind":"dsse","version":"0.0.1"},"logId":{"keyId":"nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE="}}]}} +{"_type":"https://in-toto.io/Statement/v1","subject":[{"name":"artifact.bin","digest":{"sha256":"e88951291bca0807a41f3c0144009bfff5c6c77dc1310d52e8c6d62feee960a8"}}],"predicateType":"https://slsa.dev/provenance/v1","predicate":{"buildDefinition":{"buildType":"https://actions.github.io/buildtypes/workflow/v1","externalParameters":{"workflow":{"ref":"refs/heads/main","repository":"https://github.com/acme/widget","path":".github/workflows/build.yml"}},"internalParameters":{"github":{"event_name":"push","repository_id":"123456789","repository_owner_id":"987654321","runner_environment":"github-hosted"}},"resolvedDependencies":[{"uri":"git+https://github.com/acme/widget@refs/heads/main","digest":{"gitCommit":"4f9d2a7c1e6b8a3d5f0e2c9b7a1d4e6f8c3b5a70"}}]},"runDetails":{"builder":{"id":"https://github.com/actions/runner/github-hosted"},"metadata":{"invocationId":"https://github.com/acme/widget/actions/runs/1234567890/attempts/1"}}}} diff --git a/internal/intoto/testdata/wrong-signer-provenance.sigstore.json b/internal/intoto/testdata/wrong-signer-provenance.sigstore.json new file mode 100644 index 000000000..7e65cc763 --- /dev/null +++ b/internal/intoto/testdata/wrong-signer-provenance.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImU4ODk1MTI5MWJjYTA4MDdhNDFmM2MwMTQ0MDA5YmZmZjVjNmM3N2RjMTMxMGQ1MmU4YzZkNjJmZWVlOTYwYTgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIGb6TKl2yC1WpnZFEA+AYdCgZcNz0PlqfAaacUthZKcCAiEAyHBuRSaM4Onk6q90CN6uljCfZMqV9SevpG09e3cMEQo=" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIIC0TCCAnegAwIBAgIIMQSpmjl3aZAwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYvSuDN9eoiuol8ZHd9MMB37Lg1tvde/R7ue+fBmIf1UAx1rT6mtOUFb0kRgQ65W2KdYTXEyuc8UHjaNxwnKv7aOCAaIwggGeMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBbBgNVHREBAf8EUTBPhk1odHRwczovL2dpdGh1Yi5jb20vbWFsbG9yeS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIQDucC9SsJ3E1IewVYsDMbN8K3mlQ5SfXF8nKFFgFJSkWAIgOG7tDU9ZJ1u9+4TuFWxh3iIn8E7ZEcKdVHtzYkdV2Hs=" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUjADAgEAMIICSQYJKoZIhvcNAQcCoIICOjCCAjYCAQMxDTALBglghkgBZQMEAgEwgYIGCyqGSIb3DQEJEAEEoHMEcTBvAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgYEU6P1C2l+a8swnV78QvCS/CC9p9ez6jMaxcRtDuunoCFQDggF8UZ0oR9BHqH5CIX6Vo/8l+vxgPMjAyNjExMTExMTEwNTJaoASkAjAAoAAxggGZMIIBlQIBATBAMDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATALBglghkgBZQMEAgGggeowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjExMTExMTEwNTJaMC8GCSqGSIb3DQEJBDEiBCBBlqXrb50krYwwyMbz3YUznqv33fe6PB/sTyV5Dsg7MTB9BgsqhkiG9w0BCRACLzFuMGwwajBoBCBUqoykfHUXJTYsvFAHbO71/Km+1YCSTyMpDoJuO0Fz5jBEMD+kPTA7MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIjAgBgNVBAMTGXNpZ3N0b3JlLXRzYS1pbnRlcm1lZGlhdGUCAQEwCgYIKoZIzj0EAwIESDBGAiEAnuvYeDoV5Kxk3YTt8yIRIF+5hEJ385D4Ytcy6duf86ECIQDi4S5U2Ge3hJy02Pp0IB4r3cV9TwQGa3Mw/dpg/C9tzA==" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjMzNjIyMzkyNzg4NmExNDg5MGI1OGEwNzkzYTQ2ZDI5MTJjODFmOWNjYjE4ZmFiMmVkOTc2MGI2ZjA5NmY5Y2EifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmOTZjNDY3Y2I2Y2MzYmU1ZGRlMGE0YzZkYTFhZDQ5NTA3NGEzYjRiZTcyMDc2NDQyYzBmNGI4NzFjMTNhMWVlIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJR2I2VEtsMnlDMVdwblpGRUErQVlkQ2daY056MFBscWZBYWFjVXRoWktjQ0FpRUF5SEJ1UlNhTTRPbms2cTkwQ042dWxqQ2ZaTXFWOVNldnBHMDllM2NNRVFvPSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXdWRU5EUVc1bFowRjNTVUpCWjBsSlRWRlRjRzFxYkROaFdrRjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlZsMlUzVkVUamxsYjJsMWIydzRXa2hrT1UxTlFqTTNUR2N4ZEhaa1pTOVNOM1ZsSzJaQ2JVbG1NVlZCZURGeVZEWnRkRThLVlVaaU1HdFNaMUUyTlZjeVMyUlpWRmhGZVhWak9GVklhbUZPZUhkdVMzWTNZVTlEUVdGSmQyZG5SMlZOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENZa0puVGxaSVVrVkNRV1k0UlZWVVFsQm9hekZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZbGRHYzJKSE9Ya0taVk01TTJGWFVtNWFXRkYyVEcxa2NHUkhhREZaYVRrellqTktjbHB0ZUhaa00wMTJXVzVXY0dKSFVYVmxWekZ6VVVoS2JGcHVUWFpoUjFab1draE5kZ3BpVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQmhTRlpwQ21SWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVUnpSME5wYzBkQlVWRkNaemM0ZDBGUlowVk1VWGR5WVVoU01HTklUVFpNZVRrd1lqSjBiR0pwTldnS1dUTlNjR0l5TlhwTWJXUndaRWRvTVZsdVZucGFXRXBxWWpJMU1GcFhOVEJNYlU1MllsUkJkVUpuYjNKQ1owVkZRVmxQTDAxQlJVMUNRMEZOU0cxb01BcGtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1doWk1qRnNURE5rY0ZwSFpHeGtSRUpTUW1kdmNrSm5SVVZCV1U4dlRVRkZWa0pGVFUxUlYyZ3dDbVJJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWFGa3lNV3hNTTJSd1drZGtiR1JET1doWk0xSndZakkxZWt3elNqRmliazEyVFZSSmVrNUVWVElLVG5wbk5VMURPV2hrU0ZKc1lsaENNR041T0hoTlFXOUhRME54UjFOTk5EbENRVTFEUVRCblFVMUZWVU5KVVVSMVkwTTVVM05LTTBVeFNXVjNWbGx6UkFwTllrNDRTek50YkZFMVUyWllSamh1UzBaR1owWktVMnRYUVVsblQwYzNkRVJWT1ZwS01YVTVLelJVZFVaWGVHZ3phVWx1T0VVM1drVmpTMlJXU0hSNkNsbHJaRll5U0hNOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9XX0sImtpbmQiOiJkc3NlIn0=", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCICvvYGJwpdXELUupODYFnNBJgaSSOJLwYfcrZlCvVvDwAiBRYw7nv8rcBXI3t1uLN3WicmVv1p/TKtV0iYcpPApwPA==" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\nYvYM+cfyHLVPs13HIGJ6EMlXI3hALK6qM4lgjg0YHnI=\n\n— rekor.localhost nk5pvzBGAiEA0k56ZATpQCRx+qGVegSeSy6dTrDztBzqrOqOQk96f5ECIQCw+I9Mtquhjcg+5+ejdxgHS7DuISZy/0qT9nUOH98LdQ==\n" + }, + "rootHash": "YvYM+cfyHLVPs13HIGJ6EMlXI3hALK6qM4lgjg0YHnI=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/testdata/wrong-subject-provenance.sigstore.json b/internal/intoto/testdata/wrong-subject-provenance.sigstore.json new file mode 100644 index 000000000..936d7052b --- /dev/null +++ b/internal/intoto/testdata/wrong-subject-provenance.sigstore.json @@ -0,0 +1,47 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoib3RoZXIuYmluIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjExZGU5NjIyNjg5NmIwZmNmNGVhNjZhMTFhNjNhNWZkOWY0ZTIxMWZlOTRiNjJiN2VlYmY0NDg1YzgxN2IyZDgifX1dLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi9wcm92ZW5hbmNlL3YxIiwicHJlZGljYXRlIjp7ImJ1aWxkRGVmaW5pdGlvbiI6eyJidWlsZFR5cGUiOiJodHRwczovL2FjdGlvbnMuZ2l0aHViLmlvL2J1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldCIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9idWlsZC55bWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6InB1c2giLCJyZXBvc2l0b3J5X2lkIjoiMTIzNDU2Nzg5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk4NzY1NDMyMSIsInJ1bm5lcl9lbnZpcm9ubWVudCI6ImdpdGh1Yi1ob3N0ZWQifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL2FjbWUvd2lkZ2V0QHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiI0ZjlkMmE3YzFlNmI4YTNkNWYwZTJjOWI3YTFkNGU2ZjhjM2I1YTcwIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQCSiU9VQAwTwUB5rrdp/4g7p0Yd2DBxPpxaluHndLHuBwIgVCkh37yQIsrJEqIkoRj8vHtD3GgEvn826ecLNK6bFV8=" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIICzjCCAnSgAwIBAgIIMwz1H/c9CZQwCgYIKoZIzj0EAwIwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMTExMTEwOTUyWhcNMjYxMTExMTEyMDUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDpz24JPQDgzJA0zpubW6BdXywR2QZ8SgLq0fQ6/x5otarfK5XPPm3QGh5ZghAjO31gBBhX+CEl8ifJosZ1htLKOCAZ8wggGbMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBRtORdn7L9qKX5ipTkvF5HunBBCOjBYBgNVHREBAf8ETjBMhkpodHRwczovL2dpdGh1Yi5jb20vYWNtZS93aWRnZXQvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAuBgorBgEEAYO/MAEMBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldDBRBgorBgEEAYO/MAEVBEMMQWh0dHBzOi8vZ2l0aHViLmNvbS9hY21lL3dpZGdldC9hY3Rpb25zL3J1bnMvMTIzNDU2Nzg5MC9hdHRlbXB0cy8xMAoGCCqGSM49BAMCA0gAMEUCIHpjGFZnYOXv9Gxqk6ExP/H6xh8NCFK43QlkN8mLj6d7AiEAvgcJAHrdMyqRTSZGCHg3fo82K/PmoysgYSydOr4GMF4=" + }, + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICUjADAgEAMIICSQYJKoZIhvcNAQcCoIICOjCCAjYCAQMxDTALBglghkgBZQMEAgEwgYIGCyqGSIb3DQEJEAEEoHMEcTBvAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgDhQCElEijKx/ShH+9qrNBfzm7yG7UNoYNwQF/bVd8pACFQDLH75RZhdrLPxaVAHONRjQdNqIJhgPMjAyNjExMTExMTEwNTJaoASkAjAAoAAxggGZMIIBlQIBATBAMDsxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEiMCAGA1UEAxMZc2lnc3RvcmUtdHNhLWludGVybWVkaWF0ZQIBATALBglghkgBZQMEAgGggeowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjExMTExMTEwNTJaMC8GCSqGSIb3DQEJBDEiBCAnYqtEvKDrUSiI1a3VaGm59XzYe1AAPV+/V0v3unTzEzB9BgsqhkiG9w0BCRACLzFuMGwwajBoBCBUqoykfHUXJTYsvFAHbO71/Km+1YCSTyMpDoJuO0Fz5jBEMD+kPTA7MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIjAgBgNVBAMTGXNpZ3N0b3JlLXRzYS1pbnRlcm1lZGlhdGUCAQEwCgYIKoZIzj0EAwIESDBGAiEAuICVJoyO4sjp2dBkRdpW7KP5My4P7OsxkSi2ZbADVvUCIQCtIk27meuFbeK+luWb6SHzrz69QAhhreaMaIPvZuf2eQ==" + } + ] + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJzcGVjIjp7ImVudmVsb3BlSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImFiMzliMTJmYjM5ZTZkOWUyMWRlMmJkOTA3NjIzMGJlNTk4NjBkODgxZGU3OWQzMzdiNGM1OTI4YWM2ODFmZDUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0MjFhMDJjN2ZkNjE4YTFiZDZkMjliNzQ4MjQ2MGQwYjc1NGU0ZmNmOWE2NDI4N2Q0Zjg0NTBkNjA2Y2VkYjMxIn0sInNpZ25hdHVyZXMiOlt7InNpZ25hdHVyZSI6Ik1FVUNJUUNTaVU5VlFBd1R3VUI1cnJkcC80ZzdwMFlkMkRCeFBweGFsdUhuZExIdUJ3SWdWQ2toMzd5UUlzckpFcUlrb1JqOHZIdEQzR2dFdm44MjZlY0xOSzZiRlY4PSIsInZlcmlmaWVyIjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZha05EUVc1VFowRjNTVUpCWjBsSlRYZDZNVWd2WXpsRFdsRjNRMmRaU1V0dldrbDZhakJGUVhkSmQwNTZSVlpOUWsxSFFURlZSVU5vVFUwS1l6SnNibU16VW5aamJWVjFXa2RXTWsxU05IZElRVmxFVmxGUlJFVjRWbnBoVjJSNlpFYzVlVnBUTVhCaWJsSnNZMjB4YkZwSGJHaGtSMVYzU0doalRncE5hbGw0VFZSRmVFMVVSWGRQVkZWNVYyaGpUazFxV1hoTlZFVjRUVlJGZVUxRVZYbFhha0ZCVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEpDbnBxTUVSQlVXTkVVV2RCUlVSd2VqSTBTbEJSUkdkNlNrRXdlbkIxWWxjMlFtUlllWGRTTWxGYU9GTm5USEV3WmxFMkwzZzFiM1JoY21aTE5WaFFVRzBLTTFGSGFEVmFaMmhCYWs4ek1XZENRbWhZSzBORmJEaHBaa3B2YzFveGFIUk1TMDlEUVZvNGQyZG5SMkpOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKU0FwblJFRlVRbWRPVmtoVFZVVkVSRUZMUW1kbmNrSm5SVVpDVVdORVFYcEJaa0puVGxaSVUwMUZSMFJCVjJkQ1VuUlBVbVJ1TjB3NWNVdFlOV2x3Vkd0MkNrWTFTSFZ1UWtKRFQycENXVUpuVGxaSVVrVkNRV1k0UlZScVFrMW9hM0J2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXVmRPZEZwVE9UTUtZVmRTYmxwWVVYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaWmJsWndZa2RSZFdWWE1YTlJTRXBzV201TmRtRkhWbWhhU0UxMllsZEdjQXBpYWtFMVFtZHZja0puUlVWQldVOHZUVUZGUWtKRGRHOWtTRkozWTNwdmRrd3pVblpoTWxaMVRHMUdhbVJIYkhaaWJrMTFXakpzTUdGSVZtbGtXRTVzQ21OdFRuWmlibEpzWW01UmRWa3lPWFJOUkhOSFEybHpSMEZSVVVKbk56aDNRVkZuUlV4UmQzSmhTRkl3WTBoTk5reDVPVEJpTW5Sc1ltazFhRmt6VW5BS1lqSTFla3h0WkhCa1IyZ3hXVzVXZWxwWVNtcGlNalV3V2xjMU1FeHRUblppVkVGMVFtZHZja0puUlVWQldVOHZUVUZGVFVKRFFVMUliV2d3WkVoQ2VncFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVhRmt5TVd4TU0yUndXa2RrYkdSRVFsSkNaMjl5UW1kRlJVRlpUeTlOUVVWV1FrVk5UVkZYYURCa1NFSjZDazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemxvV1RJeGJFd3paSEJhUjJSc1pFTTVhRmt6VW5CaU1qVjZURE5LTVdKdVRYWk5WRWw2VGtSVk1rNTZaelVLVFVNNWFHUklVbXhpV0VJd1kzazRlRTFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbEljR3BIUmxwdVdVOVlkamxIZUhGck5rVjRVQzlJTmdwNGFEaE9RMFpMTkROUmJHdE9PRzFNYWpaa04wRnBSVUYyWjJOS1FVaHlaRTE1Y1ZKVVUxcEhRMGhuTTJadk9ESkxMMUJ0YjNseloxbFRlV1JQY2pSSENrMUdORDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfSwia2luZCI6ImRzc2UifQ==", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIAzHIumV2UgwGl/6MygEAn4XvyZxODCKQetRTFWDi6YuAiAtSfnaB/Ks4qGf33S2aeXcEcfvgrhwJTK3Mj/hJG/+YQ==" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.localhost - 123\n42\n/NPaMj2Yei/q0TMWbA4w2d9Tjs9X3NBOVhmLrlbykzk=\n\n— rekor.localhost nk5pvzBFAiEAjH8NzVziR/gvFzExoJitzLiNrkYYQLDK+Fqu4IIeAjUCIGf6989JPfoTgNtxI3I3c5UsVvk/bvBTxy+wasUQw8uY\n" + }, + "rootHash": "/NPaMj2Yei/q0TMWbA4w2d9Tjs9X3NBOVhmLrlbykzk=", + "treeSize": "1" + }, + "integratedTime": "1794395512", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "nk5pvxPUJXOJVERUv1asmJxXX325yYmhmfXwfS/mGpE=" + } + } + ] + } +} diff --git a/internal/intoto/trust.go b/internal/intoto/trust.go new file mode 100644 index 000000000..9d397619d --- /dev/null +++ b/internal/intoto/trust.go @@ -0,0 +1,132 @@ +package intoto + +import ( + _ "embed" + "fmt" + "sync" + + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/sigstore/sigstore-go/pkg/verify" +) + +const ( + publicGoodIssuerOrg = "sigstore.dev" + githubIssuerOrg = "GitHub, Inc." + githubTUFMirror = "https://tuf-repo.github.com" +) + +// githubTUFRoot is the root of trust for GitHub's TUF repository, as the GitHub +// CLI embeds it. TUF fetches every later root version from the mirror and +// checks each against the one before, so an old root here is still a sound +// starting point. +// +//go:embed github-tuf-root.json +var githubTUFRoot []byte + +// TrustRoots supplies the Sigstore trust roots signatures are checked against. +type TrustRoots struct { + fixed *root.TrustedRoot + + publicGood cachedRoot + github cachedRoot +} + +type cachedRoot struct { + once sync.Once + fetch func() (*root.TrustedRoot, error) + root *root.TrustedRoot + err error +} + +func (c *cachedRoot) get() (*root.TrustedRoot, error) { + c.once.Do(func() { c.root, c.err = c.fetch() }) + return c.root, c.err +} + +// SigstoreTrustRoots fetches trust roots over TUF the first time a signature +// needs one: Sigstore's public-good instance, and GitHub's own, which signs +// attestations for private repositories. Nothing is fetched for unsigned +// attestations or for ones signed with a key and carrying no timestamps. +func SigstoreTrustRoots() *TrustRoots { + return &TrustRoots{ + publicGood: cachedRoot{fetch: func() (*root.TrustedRoot, error) { + return root.FetchTrustedRootWithOptions(tuf.DefaultOptions()) + }}, + github: cachedRoot{fetch: fetchGitHubTrustedRoot}, + } +} + +// FixedTrustRoots checks every signature against one trusted root, which must +// then hold every authority a signature relies on. +func FixedTrustRoots(trustedRoot *root.TrustedRoot) *TrustRoots { + return &TrustRoots{fixed: trustedRoot} +} + +func fetchGitHubTrustedRoot() (*root.TrustedRoot, error) { + opts := tuf.DefaultOptions() + opts.Root = githubTUFRoot + opts.RepositoryBaseURL = githubTUFMirror + client, err := tuf.New(opts) + if err != nil { + return nil, err + } + content, err := client.GetTarget("trusted_root.json") + if err != nil { + return nil, err + } + return root.NewTrustedRootFromJSON(content) +} + +// forCertificate returns the trust root for a signature whose Fulcio +// certificate was issued by issuerOrg, and the evidence that instance's +// signatures must carry: public-good signatures are logged in Rekor, while +// GitHub's are timestamped by its own authority instead. +func (t *TrustRoots) forCertificate(issuerOrg string) (root.TrustedMaterial, []verify.VerifierOption, error) { + if t.fixed != nil { + return t.fixed, fixedRootOptions(t.fixed), nil + } + switch issuerOrg { + case publicGoodIssuerOrg: + trustedRoot, err := t.publicGood.get() + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch Sigstore's public-good trust root: %w", err) + } + return trustedRoot, []verify.VerifierOption{ + verify.WithSignedCertificateTimestamps(1), + verify.WithTransparencyLog(1), + verify.WithObserverTimestamps(1), + }, nil + case githubIssuerOrg: + trustedRoot, err := t.github.get() + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch GitHub's Sigstore trust root: %w", err) + } + return trustedRoot, []verify.VerifierOption{verify.WithSignedTimestamps(1)}, nil + } + return nil, nil, fmt.Errorf("the signing certificate was issued by %q, which is neither Sigstore's public-good instance nor GitHub's", issuerOrg) +} + +// forKey returns the trust root for the transparency log entries and +// timestamps carried by a signature made with a key. +func (t *TrustRoots) forKey() (*root.TrustedRoot, error) { + if t.fixed != nil { + return t.fixed, nil + } + trustedRoot, err := t.publicGood.get() + if err != nil { + return nil, fmt.Errorf("failed to fetch Sigstore's public-good trust root: %w", err) + } + return trustedRoot, nil +} + +// fixedRootOptions asks for the evidence a fixed root can check: a log entry +// when it names a log, and a trusted time from the log or a timestamp +// authority either way. +func fixedRootOptions(trustedRoot *root.TrustedRoot) []verify.VerifierOption { + opts := []verify.VerifierOption{verify.WithObserverTimestamps(1)} + if len(trustedRoot.RekorLogs()) > 0 { + opts = append(opts, verify.WithTransparencyLog(1)) + } + return opts +} diff --git a/internal/intoto/verify.go b/internal/intoto/verify.go new file mode 100644 index 000000000..6bdf84dce --- /dev/null +++ b/internal/intoto/verify.go @@ -0,0 +1,371 @@ +package intoto + +import ( + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net/url" + "os" + "regexp" + "strings" + "time" + + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + protodsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" +) + +// Policy says whose signature a signed attestation must carry. There is no +// way to accept any valid certificate: a signer is always pinned. +type Policy struct { + CertificateIdentity string + CertificateIdentityRegexp string + CertificateOIDCIssuer string + // KeyPath names a PEM public key, for attestations signed with a key + // rather than a Fulcio certificate. + KeyPath string + // AllowUnsigned accepts attestations with no signature, recorded as + // unverified. + AllowUnsigned bool +} + +// Validate checks the policy's parts make sense together. It needs nothing +// from an attestation, so a caller can run it before reading one. +func (p Policy) Validate() error { + hasIdentity := p.CertificateIdentity != "" || p.CertificateIdentityRegexp != "" + switch { + case p.CertificateIdentity != "" && p.CertificateIdentityRegexp != "": + return fmt.Errorf("only one of --certificate-identity, --certificate-identity-regexp is allowed") + case p.KeyPath != "" && (hasIdentity || p.CertificateOIDCIssuer != ""): + return fmt.Errorf("--key cannot be combined with --certificate-identity, --certificate-identity-regexp or --certificate-oidc-issuer: a signature is made either with a key or with a certificate") + case hasIdentity && p.CertificateOIDCIssuer == "": + return fmt.Errorf("--certificate-oidc-issuer is required with --certificate-identity or --certificate-identity-regexp") + case p.CertificateOIDCIssuer != "" && !hasIdentity: + return fmt.Errorf("--certificate-identity or --certificate-identity-regexp is required with --certificate-oidc-issuer") + } + if p.CertificateIdentityRegexp != "" { + if _, err := regexp.Compile(p.CertificateIdentityRegexp); err != nil { + return fmt.Errorf("--certificate-identity-regexp is not a valid regular expression: %w", err) + } + } + return nil +} + +// PinsSigner reports whether the policy can verify a signature at all. +func (p Policy) PinsSigner() bool { + return p.KeyPath != "" || p.CertificateOIDCIssuer != "" +} + +// Evidence is what checking an attestation established. +type Evidence struct { + // Verified is false only for an unsigned attestation the policy allowed. + Verified bool + // SignerIdentity and SignerIssuer are the certificate's subject + // alternative name and OIDC issuer; empty for a key signature. + SignerIdentity string + SignerIssuer string + // LogIndex, IntegratedTime and LogEntryURL describe the verified + // transparency log entry, when there is one. + LogIndex *int64 + IntegratedTime time.Time + LogEntryURL string + // SignedTime is the time of a verified RFC 3161 timestamp. + SignedTime time.Time + // RawSHA256 is the SHA-256 of the bytes the attestation was read from. + RawSHA256 string +} + +// Time is the trusted time of the signature, or zero when nothing vouches for +// one. +func (e *Evidence) Time() time.Time { + if !e.IntegratedTime.IsZero() { + return e.IntegratedTime + } + return e.SignedTime +} + +// ErrFailedVerification marks a signed attestation whose signature was +// checked and did not verify against the policy: made by another signer, +// changed after signing, or without the log or timestamp evidence its trust +// root requires. +var ErrFailedVerification = errors.New("failed verification") + +// ErrCannotVerify marks a signed attestation whose signature could not be +// checked at all: a trust root could not be fetched or does not cover the +// certificate's issuer, the policy's method does not fit how it was signed, or +// the key could not be read. It says nothing about whether the signature is +// good. +var ErrCannotVerify = errors.New("could not be verified") + +// Verifier checks attestations against a policy. +type Verifier struct { + policy Policy + trust *TrustRoots +} + +// NewVerifier returns a Verifier that checks signatures against trust. +func NewVerifier(policy Policy, trust *TrustRoots) *Verifier { + return &Verifier{policy: policy, trust: trust} +} + +// Verify checks that a names the artifact with SHA-256 digest as its subject +// and, when a is signed, that the signature is valid and made by the signer +// the policy pins. An empty digest checks the signature alone, for an +// attestation recorded on a trail rather than an artifact. A signature that +// does not verify is an ErrFailedVerification; one that cannot be checked is +// an ErrCannotVerify. +func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { + if digest != "" { + if err := a.RequireSubject(digest); err != nil { + return nil, err + } + } + rawSum := sha256.Sum256(a.Raw) + evidence := &Evidence{RawSHA256: hex.EncodeToString(rawSum[:])} + + if !a.Signed() { + if !v.policy.AllowUnsigned { + return nil, fmt.Errorf("%s is not signed. Pass --allow-unsigned to record it as unverified", a.Describe()) + } + return evidence, nil + } + if !v.policy.PinsSigner() { + return nil, fmt.Errorf("%s is signed, so its signer must be pinned to verify it. Pass --certificate-identity or --certificate-identity-regexp with --certificate-oidc-issuer, or --key", a.Describe()) + } + + verifier, entity, policy, err := v.signatureCheck(a, digest) + if err != nil { + return nil, fmt.Errorf("%s %w: %w", a.Describe(), ErrCannotVerify, err) + } + result, err := verifier.Verify(entity, policy) + if err != nil { + return nil, fmt.Errorf("%s %w: %w", a.Describe(), ErrFailedVerification, err) + } + + evidence.Verified = true + if summary := result.Signature.Certificate; summary != nil { + evidence.SignerIdentity = summary.SubjectAlternativeName + evidence.SignerIssuer = summary.Issuer + } + for _, ts := range result.VerifiedTimestamps { + switch ts.Type { + case "Tlog": + evidence.IntegratedTime = ts.Timestamp.UTC() + case "TimestampAuthority": + evidence.SignedTime = ts.Timestamp.UTC() + } + } + v.recordLogEntry(evidence, entity) + return evidence, nil +} + +// signatureCheck prepares checking a's signature: the verifier, with the +// trust material and evidence the signer's instance requires, the signed +// entity, and the policy to check it against. An error here means the +// signature cannot be checked, not that it is bad. +func (v *Verifier) signatureCheck(a *Attestation, digest string) (*verify.Verifier, *bundle.Bundle, verify.PolicyBuilder, error) { + artifactPolicy := verify.WithoutArtifactUnsafe() + if digest != "" { + digestBytes, err := hex.DecodeString(normaliseDigest(digest)) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, fmt.Errorf("the artifact digest is not hexadecimal: %w", err) + } + artifactPolicy = verify.WithArtifactDigest("sha256", digestBytes) + } + + if v.policy.KeyPath != "" { + return v.keyCheck(a, artifactPolicy) + } + + if a.bundle == nil { + return nil, nil, verify.PolicyBuilder{}, fmt.Errorf("a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope with --key") + } + leaf, err := signingCertificate(a.bundle) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + if leaf == nil { + return nil, nil, verify.PolicyBuilder{}, fmt.Errorf("it is signed with a key, not a certificate. Verify it with --key") + } + issuerOrg := "" + if len(leaf.Issuer.Organization) > 0 { + issuerOrg = leaf.Issuer.Organization[0] + } + trustedMaterial, opts, err := v.trust.forCertificate(issuerOrg) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + identity, err := verify.NewShortCertificateIdentity(v.policy.CertificateOIDCIssuer, "", v.policy.CertificateIdentity, v.policy.CertificateIdentityRegexp) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + verifier, err := verify.NewVerifier(trustedMaterial, opts...) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + return verifier, a.bundle, verify.NewPolicy(artifactPolicy, verify.WithCertificateIdentity(identity)), nil +} + +func (v *Verifier) keyCheck(a *Attestation, artifactPolicy verify.ArtifactPolicyOption) (*verify.Verifier, *bundle.Bundle, verify.PolicyBuilder, error) { + key, err := loadKey(v.policy.KeyPath) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + keyMaterial := root.NewTrustedPublicKeyMaterial(func(string) (root.TimeConstrainedVerifier, error) { + return key, nil + }) + + entity := a.bundle + if entity == nil { + entity, err = envelopeAsBundle(a) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + } else { + leaf, err := signingCertificate(entity) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + if leaf != nil { + return nil, nil, verify.PolicyBuilder{}, fmt.Errorf("it is signed with a certificate, not a key. Verify it with --certificate-identity or --certificate-identity-regexp with --certificate-oidc-issuer") + } + } + + var trustedMaterial root.TrustedMaterial = keyMaterial + var opts []verify.VerifierOption + tlogEntries, err := entity.TlogEntries() + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + timestamps, err := entity.Timestamps() + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + switch { + case len(tlogEntries) > 0: + opts = []verify.VerifierOption{verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)} + case len(timestamps) > 0: + opts = []verify.VerifierOption{verify.WithSignedTimestamps(1)} + default: + opts = []verify.VerifierOption{verify.WithNoObserverTimestamps()} + } + if len(tlogEntries) > 0 || len(timestamps) > 0 { + trustRoot, err := v.trust.forKey() + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + trustedMaterial = root.TrustedMaterialCollection{trustRoot, keyMaterial} + } + + verifier, err := verify.NewVerifier(trustedMaterial, opts...) + if err != nil { + return nil, nil, verify.PolicyBuilder{}, err + } + return verifier, entity, verify.NewPolicy(artifactPolicy, verify.WithKey()), nil +} + +// signingCertificate returns the certificate b was signed with, or nil when b +// was signed with a key. +func signingCertificate(b *bundle.Bundle) (*x509.Certificate, error) { + content, err := b.VerificationContent() + if err != nil { + return nil, err + } + return content.Certificate(), nil +} + +// envelopeAsBundle puts a bare DSSE envelope in a bundle whose verification +// material is a key hint, which is how sigstore-go verifies a key signature. +func envelopeAsBundle(a *Attestation) (*bundle.Bundle, error) { + payload, err := a.envelope.DecodeB64Payload() + if err != nil { + return nil, err + } + signatures := make([]*protodsse.Signature, 0, len(a.envelope.Signatures)) + for _, s := range a.envelope.Signatures { + sig, err := base64.StdEncoding.DecodeString(s.Sig) + if err != nil { + return nil, fmt.Errorf("the DSSE envelope's signature is not valid base64: %w", err) + } + signatures = append(signatures, &protodsse.Signature{Sig: sig, Keyid: s.KeyID}) + } + mediaType, err := bundle.MediaTypeString("0.3") + if err != nil { + return nil, err + } + return bundle.NewBundle(&protobundle.Bundle{ + MediaType: mediaType, + VerificationMaterial: &protobundle.VerificationMaterial{ + Content: &protobundle.VerificationMaterial_PublicKey{PublicKey: &protocommon.PublicKeyIdentifier{}}, + }, + Content: &protobundle.Bundle_DsseEnvelope{DsseEnvelope: &protodsse.Envelope{ + Payload: payload, + PayloadType: a.envelope.PayloadType, + Signatures: signatures, + }}, + }) +} + +func loadKey(path string) (root.TimeConstrainedVerifier, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read --key %s: %w", path, err) + } + publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(content) + if err != nil { + return nil, fmt.Errorf("--key %s is not a PEM public key: %w", path, err) + } + verifier, err := signature.LoadVerifier(publicKey, crypto.SHA256) + if err != nil { + return nil, fmt.Errorf("--key %s cannot verify signatures: %w", path, err) + } + return root.NewExpiringKey(verifier, time.Time{}, time.Time{}), nil +} + +// recordLogEntry adds the transparency log entry, already verified, so a +// reader can find it in the log. +func (v *Verifier) recordLogEntry(evidence *Evidence, entity *bundle.Bundle) { + entries, err := entity.TlogEntries() + if err != nil || len(entries) == 0 { + return + } + entry := entries[0] + index := entry.LogIndex() + evidence.LogIndex = &index + if entry.IsRekorV2() { + return + } + evidence.LogEntryURL = v.logEntryURL(hex.EncodeToString([]byte(entry.LogKeyID())), index) +} + +// logEntryURL links public-good entries to Sigstore's search page, which a +// person can read, and any other log's to its API. +func (v *Verifier) logEntryURL(logID string, index int64) string { + var logs map[string]*root.TransparencyLog + if v.trust.fixed != nil { + logs = v.trust.fixed.RekorLogs() + } else if trustedRoot, err := v.trust.publicGood.get(); err == nil { + logs = trustedRoot.RekorLogs() + } + log, ok := logs[logID] + if !ok || log.BaseURL == "" { + return "" + } + base, err := url.Parse(log.BaseURL) + if err != nil { + return "" + } + if base.Hostname() == "rekor.sigstore.dev" { + return fmt.Sprintf("https://search.sigstore.dev/?logIndex=%d", index) + } + return fmt.Sprintf("%s/api/v1/log/entries?logIndex=%d", strings.TrimSuffix(log.BaseURL, "/"), index) +} diff --git a/internal/intoto/verify_test.go b/internal/intoto/verify_test.go new file mode 100644 index 000000000..392204a6d --- /dev/null +++ b/internal/intoto/verify_test.go @@ -0,0 +1,381 @@ +package intoto + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/require" +) + +var pinnedSigner = Policy{CertificateIdentity: fixtureIdentity, CertificateOIDCIssuer: fixtureIssuer} + +func fixtureTrust(t *testing.T) *TrustRoots { + t.Helper() + trustedRoot, err := root.NewTrustedRootFromPath(filepath.Join("testdata", "trusted_root.json")) + require.NoError(t, err) + return FixedTrustRoots(trustedRoot) +} + +func readOne(t *testing.T, path string) *Attestation { + t.Helper() + content, err := os.ReadFile(path) + require.NoError(t, err) + attestations, err := Read(content) + require.NoError(t, err) + require.Len(t, attestations, 1) + return attestations[0] +} + +func verifyFixture(t *testing.T, policy Policy, name string) (*Evidence, error) { + t.Helper() + return NewVerifier(policy, fixtureTrust(t)).Verify(readOne(t, filepath.Join("testdata", name)), artifactDigest) +} + +func TestVerifySignedBundle(t *testing.T) { + t.Run("verifies a bundle signed by the pinned identity and records the evidence", func(t *testing.T) { + evidence, err := verifyFixture(t, pinnedSigner, "github-provenance.sigstore.json") + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join("testdata", "github-provenance.sigstore.json")) + require.NoError(t, err) + sum := sha256.Sum256(content) + + require.True(t, evidence.Verified) + require.Equal(t, fixtureIdentity, evidence.SignerIdentity) + require.Equal(t, fixtureIssuer, evidence.SignerIssuer) + require.NotNil(t, evidence.LogIndex) + require.Equal(t, int64(0), *evidence.LogIndex) + require.False(t, evidence.IntegratedTime.IsZero()) + require.False(t, evidence.SignedTime.IsZero()) + require.Equal(t, "https://rekor.localhost/api/v1/log/entries?logIndex=0", evidence.LogEntryURL) + require.Equal(t, hex.EncodeToString(sum[:]), evidence.RawSHA256) + }) + + t.Run("accepts an identity matched by a regular expression", func(t *testing.T) { + policy := Policy{CertificateIdentityRegexp: `^https://github\.com/acme/widget/`, CertificateOIDCIssuer: fixtureIssuer} + _, err := verifyFixture(t, policy, "github-provenance.sigstore.json") + require.NoError(t, err) + }) + + for _, name := range []string{"buildkit-provenance.sigstore.json", "spdx-sbom.sigstore.json", "cyclonedx-sbom.sigstore.json"} { + t.Run("verifies "+name, func(t *testing.T) { + _, err := verifyFixture(t, pinnedSigner, name) + require.NoError(t, err) + }) + } + + t.Run("refuses a bundle signed by another identity", func(t *testing.T) { + _, err := verifyFixture(t, pinnedSigner, "wrong-signer-provenance.sigstore.json") + require.ErrorIs(t, err, ErrFailedVerification) + require.NotErrorIs(t, err, ErrCannotVerify) + require.ErrorContains(t, err, "the attestation failed verification") + require.ErrorContains(t, err, fixtureOtherIdentity) + }) + + t.Run("refuses a bundle with the wrong OIDC issuer", func(t *testing.T) { + policy := Policy{CertificateIdentity: fixtureIdentity, CertificateOIDCIssuer: "https://accounts.example.com"} + _, err := verifyFixture(t, policy, "github-provenance.sigstore.json") + require.ErrorContains(t, err, "the attestation failed verification") + }) + + t.Run("refuses a bundle whose statement was changed after signing", func(t *testing.T) { + _, err := verifyFixture(t, pinnedSigner, "tampered-provenance.sigstore.json") + require.ErrorIs(t, err, ErrFailedVerification) + require.NotErrorIs(t, err, ErrCannotVerify) + require.ErrorContains(t, err, "the attestation failed verification") + }) + + t.Run("refuses a signed statement about another artifact", func(t *testing.T) { + _, err := verifyFixture(t, pinnedSigner, "wrong-subject-provenance.sigstore.json") + require.EqualError(t, err, "the attestation does not list the artifact (sha256:"+artifactDigest+") as its subject") + }) + + t.Run("refuses a signed bundle when no signer is pinned, naming the flags that pin one", func(t *testing.T) { + _, err := verifyFixture(t, Policy{}, "github-provenance.sigstore.json") + require.EqualError(t, err, "the attestation is signed, so its signer must be pinned to verify it. Pass --certificate-identity or --certificate-identity-regexp with --certificate-oidc-issuer, or --key") + }) + + t.Run("cannot check a certificate signature with --key, and says so rather than calling it a bad signature", func(t *testing.T) { + _, err := verifyFixture(t, Policy{KeyPath: filepath.Join("testdata", "key.pub")}, "github-provenance.sigstore.json") + require.ErrorIs(t, err, ErrCannotVerify) + require.NotErrorIs(t, err, ErrFailedVerification) + require.EqualError(t, err, "the attestation could not be verified: it is signed with a certificate, not a key. Verify it with --certificate-identity or --certificate-identity-regexp with --certificate-oidc-issuer") + }) +} + +func TestVerifyKeySignedEnvelope(t *testing.T) { + t.Run("verifies a DSSE envelope with the key that signed it", func(t *testing.T) { + evidence, err := verifyFixture(t, Policy{KeyPath: filepath.Join("testdata", "key.pub")}, "key-signed-provenance.dsse.json") + require.NoError(t, err) + require.True(t, evidence.Verified) + require.Empty(t, evidence.SignerIdentity) + require.Nil(t, evidence.LogIndex) + }) + + t.Run("refuses a DSSE envelope checked with another key", func(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + pem, err := cryptoutils.MarshalPublicKeyToPEM(&key.PublicKey) + require.NoError(t, err) + keyPath := filepath.Join(t.TempDir(), "other.pub") + require.NoError(t, os.WriteFile(keyPath, pem, 0o600)) + + _, err = verifyFixture(t, Policy{KeyPath: keyPath}, "key-signed-provenance.dsse.json") + require.ErrorIs(t, err, ErrFailedVerification) + require.ErrorContains(t, err, "the attestation failed verification") + }) + + t.Run("explains that a DSSE envelope has no certificate to check an identity against", func(t *testing.T) { + _, err := verifyFixture(t, pinnedSigner, "key-signed-provenance.dsse.json") + require.ErrorIs(t, err, ErrCannotVerify) + require.NotErrorIs(t, err, ErrFailedVerification) + require.EqualError(t, err, "the attestation could not be verified: a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope with --key") + }) + + t.Run("explains that a key-signed bundle has no certificate to check an identity against", func(t *testing.T) { + envelope := readOne(t, filepath.Join("testdata", "key-signed-provenance.dsse.json")) + keySigned, err := envelopeAsBundle(envelope) + require.NoError(t, err) + content, err := keySigned.MarshalJSON() + require.NoError(t, err) + attestations, err := Read(content) + require.NoError(t, err) + + _, err = NewVerifier(pinnedSigner, fixtureTrust(t)).Verify(attestations[0], artifactDigest) + require.ErrorIs(t, err, ErrCannotVerify) + require.EqualError(t, err, "the attestation could not be verified: it is signed with a key, not a certificate. Verify it with --key") + }) + + t.Run("names a key file that is not a public key", func(t *testing.T) { + _, err := verifyFixture(t, Policy{KeyPath: filepath.Join("testdata", "artifact.bin")}, "key-signed-provenance.dsse.json") + require.ErrorIs(t, err, ErrCannotVerify) + require.ErrorContains(t, err, "--key testdata/artifact.bin is not a PEM public key") + }) +} + +// issuedBy returns the named bundle fixture with its signing certificate +// replaced by a throwaway one issued by org, so a test can steer which trust +// root the signature needs. The signature no longer matches the certificate, +// so the bundle can only be used where verification stops before checking it. +func issuedBy(t *testing.T, name, org string) *Attestation { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{Organization: []string{org}}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join("testdata", name)) + require.NoError(t, err) + var b map[string]any + require.NoError(t, json.Unmarshal(content, &b)) + b["verificationMaterial"].(map[string]any)["certificate"] = map[string]any{"rawBytes": base64.StdEncoding.EncodeToString(der)} + content, err = json.Marshal(b) + require.NoError(t, err) + attestations, err := Read(content) + require.NoError(t, err) + return attestations[0] +} + +func TestVerifyWhenTheSignatureCannotBeChecked(t *testing.T) { + failingFetch := func() (*root.TrustedRoot, error) { return nil, errors.New("connection refused") } + + t.Run("reports a trust root that cannot be fetched, not a bad signature", func(t *testing.T) { + trust := &TrustRoots{publicGood: cachedRoot{fetch: failingFetch}, github: cachedRoot{fetch: failingFetch}} + _, err := NewVerifier(pinnedSigner, trust).Verify(issuedBy(t, "github-provenance.sigstore.json", publicGoodIssuerOrg), artifactDigest) + require.ErrorIs(t, err, ErrCannotVerify) + require.NotErrorIs(t, err, ErrFailedVerification) + require.EqualError(t, err, "the attestation could not be verified: failed to fetch Sigstore's public-good trust root: connection refused") + }) + + t.Run("reports a certificate authority it has no trust root for, not a bad signature", func(t *testing.T) { + _, err := NewVerifier(pinnedSigner, SigstoreTrustRoots()).Verify(issuedBy(t, "github-provenance.sigstore.json", "Example, Inc."), artifactDigest) + require.ErrorIs(t, err, ErrCannotVerify) + require.EqualError(t, err, `the attestation could not be verified: the signing certificate was issued by "Example, Inc.", which is neither Sigstore's public-good instance nor GitHub's`) + }) +} + +func TestVerifyUnsigned(t *testing.T) { + t.Run("refuses an unsigned statement unless unsigned ones are allowed", func(t *testing.T) { + _, err := verifyFixture(t, Policy{}, filepath.Join("statements", "buildkit-provenance.json")) + require.EqualError(t, err, "the attestation is not signed. Pass --allow-unsigned to record it as unverified") + }) + + t.Run("accepts an unsigned statement as unverified when allowed", func(t *testing.T) { + evidence, err := verifyFixture(t, Policy{AllowUnsigned: true}, filepath.Join("statements", "buildkit-provenance.json")) + require.NoError(t, err) + require.False(t, evidence.Verified) + require.True(t, evidence.Time().IsZero()) + }) + + t.Run("still checks the subject of an unsigned statement", func(t *testing.T) { + _, err := verifyFixture(t, Policy{AllowUnsigned: true}, filepath.Join("statements", "wrong-subject-provenance.json")) + require.ErrorContains(t, err, "does not list the artifact") + }) +} + +func TestPolicyValidate(t *testing.T) { + for _, tc := range []struct { + name string + policy Policy + want string + }{ + {"identity and identity regexp together", Policy{CertificateIdentity: "a", CertificateIdentityRegexp: "b", CertificateOIDCIssuer: "c"}, + "only one of --certificate-identity, --certificate-identity-regexp is allowed"}, + {"identity without issuer", Policy{CertificateIdentity: "a"}, + "--certificate-oidc-issuer is required with --certificate-identity or --certificate-identity-regexp"}, + {"issuer without identity", Policy{CertificateOIDCIssuer: "c"}, + "--certificate-identity or --certificate-identity-regexp is required with --certificate-oidc-issuer"}, + {"key with identity", Policy{KeyPath: "k", CertificateIdentity: "a", CertificateOIDCIssuer: "c"}, + "--key cannot be combined with --certificate-identity, --certificate-identity-regexp or --certificate-oidc-issuer: a signature is made either with a key or with a certificate"}, + {"invalid regexp", Policy{CertificateIdentityRegexp: "(", CertificateOIDCIssuer: "c"}, + "--certificate-identity-regexp is not a valid regular expression: error parsing regexp: missing closing ): `(`"}, + } { + t.Run("refuses "+tc.name, func(t *testing.T) { + require.EqualError(t, tc.policy.Validate(), tc.want) + }) + } + + t.Run("accepts no policy at all, for unsigned input", func(t *testing.T) { + require.NoError(t, Policy{}.Validate()) + }) +} + +// jsonlFile writes the named fixtures, one per line, as an .intoto.jsonl file +// and returns its path. A name under statements/ is an unsigned statement. +func jsonlFile(t *testing.T, names ...string) string { + t.Helper() + lines := make([][]byte, len(names)) + for i, name := range names { + content, err := os.ReadFile(filepath.Join("testdata", name)) + require.NoError(t, err) + lines[i] = compact(t, content) + } + path := filepath.Join(t.TempDir(), "attestations.intoto.jsonl") + require.NoError(t, os.WriteFile(path, append(bytes.Join(lines, []byte("\n")), '\n'), 0o600)) + return path +} + +func TestFind(t *testing.T) { + find := func(t *testing.T, policy Policy, kind PredicateKind, name string) (*Found, error) { + return Find(BundleFile{Path: filepath.Join("testdata", name)}, kind, artifactDigest, NewVerifier(policy, fixtureTrust(t))) + } + + t.Run("finds the attestation of the kind asked for among several", func(t *testing.T) { + found, err := find(t, pinnedSigner, KindProvenance, "attestations.intoto.jsonl") + require.NoError(t, err) + require.Equal(t, 2, found.Position) + require.True(t, found.Evidence.Verified) + }) + + t.Run("chooses the newest by its logged time, not its place in the file", func(t *testing.T) { + found, err := find(t, pinnedSigner, KindProvenance, "newest-first.intoto.jsonl") + require.NoError(t, err) + require.Equal(t, 1, found.Position) + }) + + t.Run("prefers a verified attestation to an unsigned one", func(t *testing.T) { + found, err := find(t, Policy{CertificateIdentity: fixtureIdentity, CertificateOIDCIssuer: fixtureIssuer, AllowUnsigned: true}, KindProvenance, "attestations.intoto.jsonl") + require.NoError(t, err) + require.True(t, found.Evidence.Verified) + }) + + t.Run("names the predicate types found when none is the kind asked for", func(t *testing.T) { + _, err := find(t, pinnedSigner, KindProvenance, "vuln-only.intoto.jsonl") + require.EqualError(t, err, "bundle file testdata/vuln-only.intoto.jsonl: found no provenance attestation (SLSA provenance v1 or v0.2); the predicate types found were: https://cosign.sigstore.dev/attestation/vuln/v1") + }) + + t.Run("refuses the file when a candidate fails verification, though an unsigned one is allowed", func(t *testing.T) { + _, err := find(t, Policy{CertificateIdentity: fixtureIdentity, CertificateOIDCIssuer: fixtureIssuer, AllowUnsigned: true}, KindProvenance, "wrong-signer-and-unsigned.intoto.jsonl") + require.ErrorIs(t, err, ErrFailedVerification) + require.ErrorContains(t, err, "bundle file testdata/wrong-signer-and-unsigned.intoto.jsonl: attestation 1 failed verification: ") + }) + + t.Run("refuses the file when a candidate fails verification, though another verifies", func(t *testing.T) { + path := jsonlFile(t, "github-provenance.sigstore.json", "tampered-provenance.sigstore.json") + + _, err := Find(BundleFile{Path: path}, KindProvenance, artifactDigest, NewVerifier(pinnedSigner, fixtureTrust(t))) + require.ErrorIs(t, err, ErrFailedVerification) + require.ErrorContains(t, err, "attestation 2 failed verification: ") + }) + + t.Run("names every candidate that failed verification", func(t *testing.T) { + path := jsonlFile(t, "wrong-signer-provenance.sigstore.json", "tampered-provenance.sigstore.json") + + _, err := Find(BundleFile{Path: path}, KindProvenance, artifactDigest, NewVerifier(pinnedSigner, fixtureTrust(t))) + require.ErrorIs(t, err, ErrFailedVerification) + require.ErrorContains(t, err, ": 2 provenance attestations could not be accepted: attestation 1 failed verification: ") + require.ErrorContains(t, err, "; attestation 2 failed verification: ") + }) + + t.Run("records nothing when a candidate cannot be verified, and says why rather than calling it a bad signature", func(t *testing.T) { + path := jsonlFile(t, "key-signed-provenance.dsse.json", "github-provenance.sigstore.json") + + _, err := Find(BundleFile{Path: path}, KindProvenance, artifactDigest, NewVerifier(pinnedSigner, fixtureTrust(t))) + require.ErrorIs(t, err, ErrCannotVerify) + require.NotErrorIs(t, err, ErrFailedVerification) + require.ErrorContains(t, err, ": attestation 1 could not be verified: a DSSE envelope carries no certificate") + }) + + t.Run("reports a merely unsigned alternative without failing", func(t *testing.T) { + path := jsonlFile(t, "github-provenance.sigstore.json", filepath.Join("statements", "github-provenance.json")) + + found, err := Find(BundleFile{Path: path}, KindProvenance, artifactDigest, NewVerifier(pinnedSigner, fixtureTrust(t))) + require.NoError(t, err) + require.Equal(t, 1, found.Position) + require.Len(t, found.Rejected, 1) + }) + + t.Run("reports why the only candidate was refused", func(t *testing.T) { + _, err := find(t, pinnedSigner, KindProvenance, "tampered-provenance.sigstore.json") + require.ErrorContains(t, err, "bundle file testdata/tampered-provenance.sigstore.json: the attestation failed verification") + }) + + t.Run("names a file that cannot be read", func(t *testing.T) { + _, err := find(t, pinnedSigner, KindProvenance, "missing.sigstore.json") + require.EqualError(t, err, "bundle file testdata/missing.sigstore.json: open testdata/missing.sigstore.json: no such file or directory") + }) +} + +func TestSigstoreTrustRoots(t *testing.T) { + t.Run("refuses a certificate from an authority it does not know, without fetching anything", func(t *testing.T) { + _, _, err := SigstoreTrustRoots().forCertificate("Example, Inc.") + require.EqualError(t, err, `the signing certificate was issued by "Example, Inc.", which is neither Sigstore's public-good instance nor GitHub's`) + }) +} + +func TestVerifyWithoutAnArtifact(t *testing.T) { + verifier := func(t *testing.T) *Verifier { return NewVerifier(pinnedSigner, fixtureTrust(t)) } + + t.Run("checks the signature alone when there is no artifact to check the subject against", func(t *testing.T) { + evidence, err := verifier(t).Verify(readOne(t, filepath.Join("testdata", "wrong-subject-provenance.sigstore.json")), "") + require.NoError(t, err) + require.True(t, evidence.Verified) + }) + + t.Run("still refuses a bad signature", func(t *testing.T) { + _, err := verifier(t).Verify(readOne(t, filepath.Join("testdata", "tampered-provenance.sigstore.json")), "") + require.ErrorContains(t, err, "the attestation failed verification") + }) +} diff --git a/internal/types/types.go b/internal/types/types.go index 7cb212bbf..667156c61 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -97,6 +97,15 @@ type Commit struct { SignedByPlatform *bool `json:"signed_by_platform,omitempty"` } +// CommitFinder looks up a commit by its SHA in a hosted git repository, so a +// commit named without its details, as a provenance statement names one, can +// be recorded with them. Each provider implements it. +type CommitFinder interface { + // CommitBySHA returns the commit with sha in the repository named by its + // path on the host, such as acme/widget. + CommitBySHA(repository, sha string) (*Commit, error) +} + type PRRetriever interface { PREvidenceForCommitV2(string) ([]*PREvidence, error) PREvidenceForCommitV1(string) ([]*PREvidence, error)