From 75b643ed7f103a8d170b68be3ab85588a704ce18 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:02:10 +0000 Subject: [PATCH 01/17] Check git_commit_info against the API's GitCommit schema attest in-toto sends commit info through gitview.BasicCommitInfo, as attest artifact, begin trail and every attestation already do. The OpenAPI contract test now covers it against the GitCommit component, so a field the CLI sends that the schema drops, or a newly required one, fails here. The published schema marks only sha1 as required, which suggests a commit known only by its SHA could leave the other fields out. The server does not accept that on POST /artifacts: it validates message, author, timestamp and branch as required, and refuses an omitted branch where it accepts an empty one. So BasicCommitInfo keeps sending every field, and attest in-toto sends no commit info at all, rather than a partial one, when it cannot find the details. --- cmd/kosli/openapiContract_test.go | 2 ++ 1 file changed, 2 insertions(+) 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 { From 36e4f08e4273d3423469d528847731ad57030210 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:05:05 +0000 Subject: [PATCH 02/17] Read in-toto attestations and match them to an artifact Add internal/intoto, the package behind attest in-toto, starting with reading and selecting attestations. Nothing is verified yet. Read accepts the three forms build systems write: a Sigstore bundle (actions/attest, cosign), a DSSE envelope, and a bare statement (buildx's default unsigned output), either alone in a file or one per line as in a .intoto.jsonl file. Statements are parsed with the in-toto attestation Go types. The predicate is also kept as the raw bytes the payload carried, because the typed Statement holds it as a protobuf Struct, which turns every number into a float64, and an SBOM handed on from it would no longer be the document that was signed. Raw holds the bytes each attestation came from, the whole file when it holds one, so the checksum recorded as evidence later is one a reader can reproduce from the file. Matching keeps the attestations of one predicate kind, provenance (SLSA v1 and v0.2) or sbom (SPDX and CycloneDX, versioned or not), that list the artifact's digest as a subject. When none does it says what it found instead, the predicate types or the subjects, since a wrong file or a wrong artifact is the likely mistake and the message is where the user finds out which. --- go.mod | 40 +++- go.sum | 192 ++++++++++++++++-- internal/intoto/match.go | 133 ++++++++++++ internal/intoto/match_test.go | 90 ++++++++ internal/intoto/read.go | 177 ++++++++++++++++ internal/intoto/read_test.go | 129 ++++++++++++ internal/intoto/testdata/artifact.bin | 1 + .../statements/buildkit-provenance.json | 37 ++++ .../testdata/statements/cyclonedx-sbom.json | 20 ++ .../statements/github-provenance.json | 34 ++++ .../statements/slsa-v0.2-provenance.json | 29 +++ .../intoto/testdata/statements/spdx-sbom.json | 29 +++ .../intoto/testdata/statements/vuln-scan.json | 12 ++ .../statements/wrong-subject-provenance.json | 34 ++++ 14 files changed, 935 insertions(+), 22 deletions(-) create mode 100644 internal/intoto/match.go create mode 100644 internal/intoto/match_test.go create mode 100644 internal/intoto/read.go create mode 100644 internal/intoto/read_test.go create mode 100644 internal/intoto/testdata/artifact.bin create mode 100644 internal/intoto/testdata/statements/buildkit-provenance.json create mode 100644 internal/intoto/testdata/statements/cyclonedx-sbom.json create mode 100644 internal/intoto/testdata/statements/github-provenance.json create mode 100644 internal/intoto/testdata/statements/slsa-v0.2-provenance.json create mode 100644 internal/intoto/testdata/statements/spdx-sbom.json create mode 100644 internal/intoto/testdata/statements/vuln-scan.json create mode 100644 internal/intoto/testdata/statements/wrong-subject-provenance.json diff --git a/go.mod b/go.mod index 87f4ea2e2..649b271f7 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,9 @@ 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/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 +83,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 +101,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 +109,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 +132,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 +153,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,12 +163,16 @@ 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/json-iterator/go v1.1.12 // indirect @@ -174,12 +194,13 @@ 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 @@ -187,6 +208,12 @@ require ( github.com/sagikazarmark/locafero v0.11.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/protobuf-specs v0.5.2 // indirect + github.com/sigstore/rekor v1.5.5 // indirect + github.com/sigstore/rekor-tiles/v2 v2.4.0 // indirect + github.com/sigstore/sigstore v1.11.1 // 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 +221,9 @@ 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/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 +232,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 +243,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..d85665d20 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,6 +334,8 @@ 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= @@ -288,6 +345,8 @@ 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 +358,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 +449,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 +475,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 +496,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 +527,43 @@ 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 v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +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 +600,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 +644,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 +653,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 +669,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 +721,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 +780,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/internal/intoto/match.go b/internal/intoto/match.go new file mode 100644 index 000000000..c9956f1df --- /dev/null +++ b/internal/intoto/match.go @@ -0,0 +1,133 @@ +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 +} + +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/read.go b/internal/intoto/read.go new file mode 100644 index 000000000..3ad659f7b --- /dev/null +++ b/internal/intoto/read.go @@ -0,0 +1,177 @@ +// 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 +} + +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..6f1f29507 --- /dev/null +++ b/internal/intoto/read_test.go @@ -0,0 +1,129 @@ +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") + }) +} 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/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"} + } + } +} From a213c4e88dcdde4d395ecafe50c794e65fd8311f Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:11:37 +0000 Subject: [PATCH 03/17] Verify signed in-toto attestations with sigstore-go A signed attestation is only worth recording if Kosli checks who signed it, so verification always pins a signer: a certificate identity (exact or by regular expression) with its OIDC issuer, or a public key. There is deliberately no way to accept any valid certificate. The flag names in the messages mirror cosign verify-attestation and gh attestation verify, so existing policies carry across. Trust roots come from TUF. A Fulcio certificate issued by Sigstore's public-good instance is checked against its trust root and must be logged in Rekor; one issued by GitHub, which signs attestations for private repositories on its own instance, is checked against GitHub's trust root and must carry an RFC 3161 timestamp instead. GitHub's TUF root is embedded, as the GitHub CLI embeds it. Roots are fetched only when a signature needs one, so unsigned input and key signatures with no timestamps never touch the network. Verification also checks the statement's subject against the artifact, both before signing is considered (so an unsigned statement about another artifact is refused too) and through sigstore-go's artifact policy. Unsigned attestations are refused unless the policy allows them, and are then marked unverified. A bare DSSE envelope has no certificate, so it can be verified only with a key. The evidence returned carries the signer, the log index, the verified integrated time and signed timestamp, a link to the log entry, and the SHA-256 of the bytes the attestation was read from. Find ties this together for a Source, the interface the GitHub and OCI sources will implement later; BundleFile is the first. When several attestations qualify it prefers a verified one, then the newest by its trusted time. The signed fixtures are generated offline by an opt-in test against a throwaway Sigstore whose trust root is committed alongside them; the README in testdata explains how to regenerate them. --- go.mod | 7 +- go.sum | 3 +- internal/intoto/find.go | 100 ++++++ internal/intoto/fixtures_test.go | 298 ++++++++++++++++ internal/intoto/github-tuf-root.json | 167 +++++++++ internal/intoto/testdata/README.md | 40 +++ .../intoto/testdata/attestations.intoto.jsonl | 3 + .../buildkit-provenance.sigstore.json | 47 +++ .../testdata/cyclonedx-sbom.sigstore.json | 47 +++ .../testdata/github-provenance.sigstore.json | 47 +++ .../testdata/key-signed-provenance.dsse.json | 10 + internal/intoto/testdata/key.pub | 4 + .../intoto/testdata/newest-first.intoto.jsonl | 2 + .../slsa-v0.2-provenance.sigstore.json | 47 +++ .../intoto/testdata/spdx-sbom.sigstore.json | 47 +++ .../tampered-provenance.sigstore.json | 47 +++ internal/intoto/testdata/trusted_root.json | 85 +++++ .../testdata/unsigned-spdx-sbom.dsse.json | 5 + .../intoto/testdata/vuln-only.intoto.jsonl | 2 + .../wrong-signer-provenance.sigstore.json | 47 +++ .../wrong-subject-provenance.sigstore.json | 47 +++ internal/intoto/trust.go | 132 +++++++ internal/intoto/verify.go | 330 ++++++++++++++++++ internal/intoto/verify_test.go | 234 +++++++++++++ 24 files changed, 1795 insertions(+), 3 deletions(-) create mode 100644 internal/intoto/find.go create mode 100644 internal/intoto/fixtures_test.go create mode 100644 internal/intoto/github-tuf-root.json create mode 100644 internal/intoto/testdata/README.md create mode 100644 internal/intoto/testdata/attestations.intoto.jsonl create mode 100644 internal/intoto/testdata/buildkit-provenance.sigstore.json create mode 100644 internal/intoto/testdata/cyclonedx-sbom.sigstore.json create mode 100644 internal/intoto/testdata/github-provenance.sigstore.json create mode 100644 internal/intoto/testdata/key-signed-provenance.dsse.json create mode 100644 internal/intoto/testdata/key.pub create mode 100644 internal/intoto/testdata/newest-first.intoto.jsonl create mode 100644 internal/intoto/testdata/slsa-v0.2-provenance.sigstore.json create mode 100644 internal/intoto/testdata/spdx-sbom.sigstore.json create mode 100644 internal/intoto/testdata/tampered-provenance.sigstore.json create mode 100644 internal/intoto/testdata/trusted_root.json create mode 100644 internal/intoto/testdata/unsigned-spdx-sbom.dsse.json create mode 100644 internal/intoto/testdata/vuln-only.intoto.jsonl create mode 100644 internal/intoto/testdata/wrong-signer-provenance.sigstore.json create mode 100644 internal/intoto/testdata/wrong-subject-provenance.sigstore.json create mode 100644 internal/intoto/trust.go create mode 100644 internal/intoto/verify.go create mode 100644 internal/intoto/verify_test.go diff --git a/go.mod b/go.mod index 649b271f7..7e3214854 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,8 @@ require ( 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 @@ -175,6 +177,7 @@ require ( 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 @@ -206,13 +209,12 @@ require ( 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/protobuf-specs v0.5.2 // indirect github.com/sigstore/rekor v1.5.5 // indirect github.com/sigstore/rekor-tiles/v2 v2.4.0 // indirect - github.com/sigstore/sigstore v1.11.1 // 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 @@ -221,6 +223,7 @@ 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 diff --git a/go.sum b/go.sum index d85665d20..09ca45ac1 100644 --- a/go.sum +++ b/go.sum @@ -341,6 +341,8 @@ github.com/google/go-github/v93 v93.0.0/go.mod h1:l8I3rt4/mdjANKpg4Y84LEy9HJQCet 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= @@ -531,7 +533,6 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB 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 v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= 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= diff --git a/internal/intoto/find.go b/internal/intoto/find.go new file mode 100644 index 000000000..470908067 --- /dev/null +++ b/internal/intoto/find.go @@ -0,0 +1,100 @@ +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, 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. +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 []error + for _, candidate := range candidates { + evidence, err := verifier.Verify(candidate, digest) + if err != nil { + rejected = append(rejected, err) + continue + } + accepted = append(accepted, &Found{Attestation: candidate, Evidence: evidence}) + } + 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, joinErrors(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 +} + +func joinErrors(errs []error) error { + messages := make([]string, len(errs)) + for i, err := range errs { + messages[i] = err.Error() + } + return errors.New(strings.Join(messages, "; ")) +} diff --git a/internal/intoto/fixtures_test.go b/internal/intoto/fixtures_test.go new file mode 100644 index 000000000..52dda650e --- /dev/null +++ b/internal/intoto/fixtures_test.go @@ -0,0 +1,298 @@ +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"), + )) + + 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/testdata/README.md b/internal/intoto/testdata/README.md new file mode 100644 index 000000000..612d1bd2c --- /dev/null +++ b/internal/intoto/testdata/README.md @@ -0,0 +1,40 @@ +# 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. | +| `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/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/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-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..1714cd4ec --- /dev/null +++ b/internal/intoto/verify.go @@ -0,0 +1,330 @@ +package intoto + +import ( + "crypto" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "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 + // RunInvocationURI is the CI run that signed, when the certificate says. + RunInvocationURI string + SignedWithKey bool + // 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 +} + +// 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. +func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { + if !a.HasSubject(digest) { + return nil, fmt.Errorf("%s does not list the artifact (sha256:%s) as its subject", a.Describe(), normaliseDigest(digest)) + } + 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()) + } + + result, entity, err := v.verifySignature(a, digest) + if err != nil { + return nil, fmt.Errorf("%s failed verification: %w", a.Describe(), err) + } + + evidence.Verified = true + evidence.SignedWithKey = v.policy.KeyPath != "" + if summary := result.Signature.Certificate; summary != nil { + evidence.SignerIdentity = summary.SubjectAlternativeName + evidence.SignerIssuer = summary.Issuer + evidence.RunInvocationURI = summary.RunInvocationURI + } + 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 +} + +func (v *Verifier) verifySignature(a *Attestation, digest string) (*verify.VerificationResult, *bundle.Bundle, error) { + digestBytes, err := hex.DecodeString(normaliseDigest(digest)) + if err != nil { + return nil, nil, fmt.Errorf("the artifact digest is not hexadecimal: %w", err) + } + artifactPolicy := verify.WithArtifactDigest("sha256", digestBytes) + + if v.policy.KeyPath != "" { + return v.verifyWithKey(a, artifactPolicy) + } + + if a.bundle == nil { + return nil, nil, fmt.Errorf("a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope with --key") + } + verificationContent, err := a.bundle.VerificationContent() + if err != nil { + return nil, nil, err + } + leaf := verificationContent.Certificate() + if leaf == nil { + return nil, nil, 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, err + } + identity, err := verify.NewShortCertificateIdentity(v.policy.CertificateOIDCIssuer, "", v.policy.CertificateIdentity, v.policy.CertificateIdentityRegexp) + if err != nil { + return nil, nil, err + } + verifier, err := verify.NewVerifier(trustedMaterial, opts...) + if err != nil { + return nil, nil, err + } + result, err := verifier.Verify(a.bundle, verify.NewPolicy(artifactPolicy, verify.WithCertificateIdentity(identity))) + return result, a.bundle, err +} + +func (v *Verifier) verifyWithKey(a *Attestation, artifactPolicy verify.ArtifactPolicyOption) (*verify.VerificationResult, *bundle.Bundle, error) { + key, err := loadKey(v.policy.KeyPath) + if err != nil { + return nil, nil, 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, err + } + } + + var trustedMaterial root.TrustedMaterial = keyMaterial + var opts []verify.VerifierOption + tlogEntries, err := entity.TlogEntries() + if err != nil { + return nil, nil, err + } + timestamps, err := entity.Timestamps() + if err != nil { + return nil, nil, 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, err + } + trustedMaterial = root.TrustedMaterialCollection{trustRoot, keyMaterial} + } + + verifier, err := verify.NewVerifier(trustedMaterial, opts...) + if err != nil { + return nil, nil, err + } + result, err := verifier.Verify(entity, verify.NewPolicy(artifactPolicy, verify.WithKey())) + return result, entity, err +} + +// 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..3bd5fa20b --- /dev/null +++ b/internal/intoto/verify_test.go @@ -0,0 +1,234 @@ +package intoto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "os" + "path/filepath" + "testing" + + "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.False(t, evidence.SignedWithKey) + require.Equal(t, fixtureIdentity, evidence.SignerIdentity) + require.Equal(t, fixtureIssuer, evidence.SignerIssuer) + require.Equal(t, fixtureRunURI, evidence.RunInvocationURI) + 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.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.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("refuses a certificate signature checked with --key", func(t *testing.T) { + _, err := verifyFixture(t, Policy{KeyPath: filepath.Join("testdata", "key.pub")}, "github-provenance.sigstore.json") + require.ErrorContains(t, err, "expected key signature, not certificate") + }) +} + +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.True(t, evidence.SignedWithKey) + 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.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.EqualError(t, err, "the attestation failed verification: a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope 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.ErrorContains(t, err, "--key testdata/artifact.bin is not a PEM public key") + }) +} + +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()) + }) +} + +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("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`) + }) +} From 6b72452b78b82434b5f56a3da5801bc48761b013 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:14:32 +0000 Subject: [PATCH 04/17] Map provenance and SBOM predicates onto Kosli's terms ProvenanceFrom reads a SLSA provenance statement into what Kosli records an artifact by: the build URL, the commit and branch, the repository and its provider, the commit URL, and the builder, build type and workflow that annotate it. It reads the statement alone and never the environment, because attest in-toto may run in a different job or workflow from the build and must record the build's commit and run, not its own. Build types are mapped where their shape is known. GitHub Actions (actions/attest and the SLSA GitHub generator, v1 and v0.2) names the workflow, ref, repository and repository ID. BuildKit names the commit and repository in its VCS metadata; its invocation ID is a build reference rather than a URL, so the run's URL comes from the builder ID that docker/build-push-action sets. BuildKit also spells invocationID and buildInvocationID against the SLSA spec, which the predicates are read leniently enough to accept. For any other build type the commit and repository come from the git dependency, and Missing says what could not be found so the caller can warn or fail. The commit URL is derived from the repository's host: GitHub, GitLab and Bitbucket Cloud are recognised, and a self-hosted host can be named through the provider the caller passes, which attest in-toto takes from --repo-provider. The mapping lives here rather than in the attest artifact payload so a future provenance attestation type on the server can reuse it as is. SBOMDocument hands the SPDX or CycloneDX predicate on as the bytes the payload carried, for internal/sbom to summarise exactly as attest sbom does. --- internal/intoto/provenance.go | 321 +++++++++++++++++++++++++++++ internal/intoto/provenance_test.go | 206 ++++++++++++++++++ internal/intoto/sbom.go | 12 ++ 3 files changed, 539 insertions(+) create mode 100644 internal/intoto/provenance.go create mode 100644 internal/intoto/provenance_test.go create mode 100644 internal/intoto/sbom.go diff --git a/internal/intoto/provenance.go b/internal/intoto/provenance.go new file mode 100644 index 000000000..951a9e1f8 --- /dev/null +++ b/internal/intoto/provenance.go @@ -0,0 +1,321 @@ +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. + Missing []string +} + +// 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 }{ + {"build URL", p.BuildURL}, + {"commit", p.Commit}, + {"commit URL", p.CommitURL}, + {"repository", 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/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 +} From 7424c3193d172c247f1a04b3a49bed0b68ae647e Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:15:28 +0000 Subject: [PATCH 05/17] Look up a commit's details on GitHub by its SHA A provenance statement names the commit an artifact was built from but not its message, author or time, and attest in-toto may run where no checkout of that commit exists. So the details are looked up in the repository the statement names. types.CommitFinder is the provider-neutral shape of that lookup, a commit by SHA in a repository named by its path on the host, returning types.Commit like the PR evidence code does. GitLab, Bitbucket and Azure DevOps can implement it later and be chosen by the repository's host. The GitHub implementation uses the REST commits endpoint through the existing client, so --github-base-url reaches GitHub Enterprise Server and --debug dumps the exchange. The author is formatted as git and the PR evidence format it, "Name ", and the message is trimmed as gitview trims it. --- internal/github/commit.go | 53 ++++++++++++++++++++++++++++ internal/github/commit_test.go | 64 ++++++++++++++++++++++++++++++++++ internal/types/types.go | 9 +++++ 3 files changed, 126 insertions(+) create mode 100644 internal/github/commit.go create mode 100644 internal/github/commit_test.go 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/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) From 93a63929774ce2279beba874ff2de593c8d3e561 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:17:36 +0000 Subject: [PATCH 06/17] Share how attest sbom builds its attestation data attest in-toto records an SBOM found in an attestation the same way attest sbom records an SBOM file, so the trail shows the same record either way. This pulls the two pieces it needs out of loadSbom without changing what attest sbom sends. sbomAttestationData takes the attached bytes and the SBOM document separately. For attest sbom they are the same file. When the SBOM arrives inside a bundle, the bundle is what is attached, and the recorded checksum has to stay the checksum of the attachment, which is the promise attest sbom makes: a reader can verify it by hand. withAnnotations replaces the annotate method's merge, keeping its reason for building a new map rather than writing into the --annotate map. --- cmd/kosli/attestSbom.go | 71 +++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/cmd/kosli/attestSbom.go b/cmd/kosli/attestSbom.go index 8de1966e7..de3fb8a05 100644 --- a/cmd/kosli/attestSbom.go +++ b/cmd/kosli/attestSbom.go @@ -213,20 +213,58 @@ 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) + o.payload.AttestationData, err = sbomAttestationData(content, content) 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)) + return content, 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 +313,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 -} From 59ddd133db9ebeac04793f73637f3ce7ca196f51 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:26:09 +0000 Subject: [PATCH 07/17] Add kosli attest in-toto, reading attestations from a bundle file Build systems already produce in-toto attestations as they build, and recording the same evidence in Kosli meant regenerating it with other attest commands, losing the signatures that make the original trustworthy. attest in-toto records the attestation itself, verified against a pinned signer, and maps its predicate onto the record Kosli already keeps for it. --predicate-type selects one kind per invocation and --name names the one resulting record, exactly as on attest artifact and attest sbom, KOSLI_NAME included: - provenance reports the artifact through the attest artifact payload, with --name as its template name (so it is held to the API's letters, digits, '-' and '_'). The commit, branch, repository and build URL come from the verified statement, never the CI environment, so the command can run in another job or workflow and still record the build's commit and run. A build or commit URL the statement lacks can be supplied with --build-url and --commit-url, and the command fails naming them when neither is available. The commit's message, author and time are looked up in the repository the statement names, on GitHub with --github-token, then read from a local checkout. Failing both, only the SHA is sent, as git_commit, with a warning that the commit's details and branch were not recorded: POST /artifacts refuses commit info missing any of them, whatever the schema says. - sbom reports an SBOM attestation built exactly as attest sbom builds one, with the bundle as its single attachment, so the recorded checksum stays the checksum of what was attached. Both carry the evidence of verification: a verified annotation, the signer, the log index and integrated time or signed timestamp, and the transparency log entry as an external URL pinned by the SHA-256 of the attestation's bytes. Provenance adds builder_id, build_type and workflow. These keys are reserved from --annotate. --source accepts only bundle for now; github and oci follow as further intoto.Source implementations. The command stays thin: reading, verification and mapping live in internal/intoto. The tests verify against the committed fixture trust root, swapped in for the TUF-backed one, and serve the commit lookup from a fake GitHub API, so none reaches the network. One records both kinds against a flow template expecting app and app.sbom and checks the trail is compliant. --- cmd/kosli/attest.go | 1 + cmd/kosli/attestInToto.go | 537 ++++++++++++++++++ cmd/kosli/attestInToto_test.go | 425 ++++++++++++++ cmd/kosli/root.go | 14 + .../testdata/empty-flag-audit-coverage.json | 29 + hack/empty-flag-audit/spec.json | 73 +++ 6 files changed, 1079 insertions(+) create mode 100644 cmd/kosli/attestInToto.go create mode 100644 cmd/kosli/attestInToto_test.go 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..84cbabea7 --- /dev/null +++ b/cmd/kosli/attestInToto.go @@ -0,0 +1,537 @@ +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. + +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 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", "", githubBaseURLFlag) + 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 + } + if provenance.Commit == "" { + return fmt.Errorf("the provenance names no git commit, and Kosli records an artifact by the commit it was built from") + } + buildURL := firstNonEmpty(provenance.BuildURL, o.buildURL) + commitURL := firstNonEmpty(provenance.CommitURL, o.commitURL) + var missingFields, missingFlags []string + if buildURL == "" { + missingFields, missingFlags = append(missingFields, "build URL"), append(missingFlags, "--build-url") + } + if commitURL == "" { + missingFields, missingFlags = append(missingFields, "commit URL"), append(missingFlags, "--commit-url") + } + 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 provenance.Repository.URL == "" { + 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..385705e9d --- /dev/null +++ b/cmd/kosli/attestInToto_test.go @@ -0,0 +1,425 @@ +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() { + 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": "[^"]+",\s+"author": "[^"]+",\s+"timestamp": \d+,\s+"branch": "main"`, + }, + }) +} + +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 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/root.go b/cmd/kosli/root.go index afc235146..cb39d14c9 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -338,6 +338,20 @@ Paths the list already matches stay excluded whatever is later added there, so k 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." + 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 + 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." 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..b81b52f35 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", diff --git a/hack/empty-flag-audit/spec.json b/hack/empty-flag-audit/spec.json index 1959442f6..06359c5e9 100644 --- a/hack/empty-flag-audit/spec.json +++ b/hack/empty-flag-audit/spec.json @@ -5095,5 +5095,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}" + } } } From 4fe746c1093baa9617c3f97193e1e146b0f2d4ac Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:32:03 +0000 Subject: [PATCH 08/17] Let attest sbom read an SBOM from a bundle or DSSE envelope actions/attest-sbom and cosign attest hand back the SBOM signed inside an in-toto attestation rather than as a plain file. attest sbom now takes either: when --sbom-file is a Sigstore bundle or DSSE envelope it reads the SPDX or CycloneDX document from the attestation, refusing any other predicate, and records it exactly as it records a plain file. The bundle or envelope is what is uploaded, so the recorded checksum is still the checksum of the file supplied, as the help has always promised, and the signed SBOM can be recovered from the attachment. Given --certificate-identity (or its regexp form) with --certificate-oidc-issuer, or --key, the attestation is verified first, the same flags and checks as attest in-toto, including that an attestation about an artifact lists that artifact as its subject. An SBOM from an attestation is annotated with verified and, when verified, with the signer, log entry and times; those keys are reserved only in that case, so plain SBOM files can still be annotated with them. Asking to verify a plain SBOM or an unsigned envelope is refused, since there is no signature to check. The help text points to attest in-toto for picking an SBOM out of a file holding several attestations, and for provenance. internal/intoto gains ReadEnvelope, which tells a bundle or envelope from anything else so a plain SBOM is never mistaken for a malformed attestation, and verification without an artifact digest for SBOMs recorded on a trail. --- cmd/kosli/attestSbom.go | 104 +++++++++++++++++- cmd/kosli/attestSbom_test.go | 83 ++++++++++++++ cmd/kosli/root.go | 2 +- .../testdata/empty-flag-audit-coverage.json | 4 + hack/empty-flag-audit/spec.json | 8 ++ internal/intoto/read.go | 22 ++++ internal/intoto/read_test.go | 39 +++++++ internal/intoto/verify.go | 16 ++- internal/intoto/verify_test.go | 15 +++ 9 files changed, 283 insertions(+), 10 deletions(-) diff --git a/cmd/kosli/attestSbom.go b/cmd/kosli/attestSbom.go index de3fb8a05..df0d0d5ee 100644 --- a/cmd/kosli/attestSbom.go +++ b/cmd/kosli/attestSbom.go @@ -7,6 +7,7 @@ import ( "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 +35,7 @@ type SbomAttestationPayload struct { type attestSbomOptions struct { *CommonAttestationOptions sbomFilePath string + policy intoto.Policy payload SbomAttestationPayload } @@ -49,8 +51,18 @@ 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. To verify its signature +first, pass ^--certificate-identity^ (or ^--certificate-identity-regexp^) with +^--certificate-oidc-issuer^, or ^--key^; an attestation about an artifact must +then list that artifact as its subject. 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 +86,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 +160,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 +178,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") @@ -214,16 +244,84 @@ func (o *attestSbomOptions) loadSbom() ([]byte, error) { return nil, fmt.Errorf("failed to read SBOM file [%s]: %s", o.sbomFilePath, err) } - o.payload.AttestationData, err = sbomAttestationData(content, 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 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 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.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 +} + // 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 diff --git a/cmd/kosli/attestSbom_test.go b/cmd/kosli/attestSbom_test.go index 729604aea..8062e70be 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,87 @@ 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"`, + }, + { + 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: "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/root.go b/cmd/kosli/root.go index cb39d14c9..1f4d55bf9 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -337,7 +337,7 @@ 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)." diff --git a/cmd/kosli/testdata/empty-flag-audit-coverage.json b/cmd/kosli/testdata/empty-flag-audit-coverage.json index b81b52f35..83f6c251f 100644 --- a/cmd/kosli/testdata/empty-flag-audit-coverage.json +++ b/cmd/kosli/testdata/empty-flag-audit-coverage.json @@ -439,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", @@ -447,6 +450,7 @@ "external-url": "stringToString", "fingerprint": "string", "flow": "string", + "key": "string", "name": "string", "origin-url": "string", "redact-commit-info": "stringSlice", diff --git a/hack/empty-flag-audit/spec.json b/hack/empty-flag-audit/spec.json index 06359c5e9..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", diff --git a/internal/intoto/read.go b/internal/intoto/read.go index 3ad659f7b..4f2c2e556 100644 --- a/internal/intoto/read.go +++ b/internal/intoto/read.go @@ -87,6 +87,28 @@ func Read(content []byte) ([]*Attestation, error) { return attestations, nil } +// 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. +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 { + 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 diff --git a/internal/intoto/read_test.go b/internal/intoto/read_test.go index 6f1f29507..65dd9072b 100644 --- a/internal/intoto/read_test.go +++ b/internal/intoto/read_test.go @@ -127,3 +127,42 @@ func TestRead(t *testing.T) { 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("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/verify.go b/internal/intoto/verify.go index 1714cd4ec..91c96f77a 100644 --- a/internal/intoto/verify.go +++ b/internal/intoto/verify.go @@ -107,9 +107,10 @@ func NewVerifier(policy Policy, trust *TrustRoots) *Verifier { // 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. +// the policy pins. An empty digest checks the signature alone, for an +// attestation recorded on a trail rather than an artifact. func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { - if !a.HasSubject(digest) { + if digest != "" && !a.HasSubject(digest) { return nil, fmt.Errorf("%s does not list the artifact (sha256:%s) as its subject", a.Describe(), normaliseDigest(digest)) } rawSum := sha256.Sum256(a.Raw) @@ -150,11 +151,14 @@ func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { } func (v *Verifier) verifySignature(a *Attestation, digest string) (*verify.VerificationResult, *bundle.Bundle, error) { - digestBytes, err := hex.DecodeString(normaliseDigest(digest)) - if err != nil { - return nil, nil, fmt.Errorf("the artifact digest is not hexadecimal: %w", err) + artifactPolicy := verify.WithoutArtifactUnsafe() + if digest != "" { + digestBytes, err := hex.DecodeString(normaliseDigest(digest)) + if err != nil { + return nil, nil, fmt.Errorf("the artifact digest is not hexadecimal: %w", err) + } + artifactPolicy = verify.WithArtifactDigest("sha256", digestBytes) } - artifactPolicy := verify.WithArtifactDigest("sha256", digestBytes) if v.policy.KeyPath != "" { return v.verifyWithKey(a, artifactPolicy) diff --git a/internal/intoto/verify_test.go b/internal/intoto/verify_test.go index 3bd5fa20b..8b9008215 100644 --- a/internal/intoto/verify_test.go +++ b/internal/intoto/verify_test.go @@ -232,3 +232,18 @@ func TestSigstoreTrustRoots(t *testing.T) { 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") + }) +} From 2a220e0ac08d7f4c4fc712d8f482e91f80b29430 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 11:53:06 +0000 Subject: [PATCH 09/17] no-mistakes(review): Drive provenance checks from Missing; drop unused fields --- cmd/kosli/attestInToto.go | 31 ++++++++++++++++++++----------- internal/intoto/provenance.go | 19 ++++++++++++++----- internal/intoto/verify.go | 5 ----- internal/intoto/verify_test.go | 3 --- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/cmd/kosli/attestInToto.go b/cmd/kosli/attestInToto.go index 84cbabea7..bb3221cb2 100644 --- a/cmd/kosli/attestInToto.go +++ b/cmd/kosli/attestInToto.go @@ -335,23 +335,32 @@ func (o *attestInTotoOptions) recordProvenance(args []string, found *intoto.Foun if err != nil { return err } - if provenance.Commit == "" { - return fmt.Errorf("the provenance names no git commit, and Kosli records an artifact by the commit it was built from") - } - buildURL := firstNonEmpty(provenance.BuildURL, o.buildURL) - commitURL := firstNonEmpty(provenance.CommitURL, o.commitURL) + buildURL, commitURL := provenance.BuildURL, provenance.CommitURL var missingFields, missingFlags []string - if buildURL == "" { - missingFields, missingFlags = append(missingFields, "build URL"), append(missingFlags, "--build-url") - } - if commitURL == "" { - missingFields, missingFlags = append(missingFields, "commit URL"), append(missingFlags, "--commit-url") + 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 provenance.Repository.URL == "" { + if noRepository { logger.Warn("the provenance does not name the repository it was built from, so no repository is recorded") } diff --git a/internal/intoto/provenance.go b/internal/intoto/provenance.go index 951a9e1f8..0bc5374b8 100644 --- a/internal/intoto/provenance.go +++ b/internal/intoto/provenance.go @@ -35,10 +35,19 @@ type Provenance struct { // 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. + // 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. @@ -178,10 +187,10 @@ func (p *Provenance) finish(repoProvider string) { } for _, field := range []struct{ name, value string }{ - {"build URL", p.BuildURL}, - {"commit", p.Commit}, - {"commit URL", p.CommitURL}, - {"repository", p.Repository.URL}, + {MissingBuildURL, p.BuildURL}, + {MissingCommit, p.Commit}, + {MissingCommitURL, p.CommitURL}, + {MissingRepository, p.Repository.URL}, } { if field.value == "" { p.Missing = append(p.Missing, field.name) diff --git a/internal/intoto/verify.go b/internal/intoto/verify.go index 91c96f77a..8aa312105 100644 --- a/internal/intoto/verify.go +++ b/internal/intoto/verify.go @@ -71,9 +71,6 @@ type Evidence struct { // alternative name and OIDC issuer; empty for a key signature. SignerIdentity string SignerIssuer string - // RunInvocationURI is the CI run that signed, when the certificate says. - RunInvocationURI string - SignedWithKey bool // LogIndex, IntegratedTime and LogEntryURL describe the verified // transparency log entry, when there is one. LogIndex *int64 @@ -132,11 +129,9 @@ func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { } evidence.Verified = true - evidence.SignedWithKey = v.policy.KeyPath != "" if summary := result.Signature.Certificate; summary != nil { evidence.SignerIdentity = summary.SubjectAlternativeName evidence.SignerIssuer = summary.Issuer - evidence.RunInvocationURI = summary.RunInvocationURI } for _, ts := range result.VerifiedTimestamps { switch ts.Type { diff --git a/internal/intoto/verify_test.go b/internal/intoto/verify_test.go index 8b9008215..0c01acc31 100644 --- a/internal/intoto/verify_test.go +++ b/internal/intoto/verify_test.go @@ -49,10 +49,8 @@ func TestVerifySignedBundle(t *testing.T) { sum := sha256.Sum256(content) require.True(t, evidence.Verified) - require.False(t, evidence.SignedWithKey) require.Equal(t, fixtureIdentity, evidence.SignerIdentity) require.Equal(t, fixtureIssuer, evidence.SignerIssuer) - require.Equal(t, fixtureRunURI, evidence.RunInvocationURI) require.NotNil(t, evidence.LogIndex) require.Equal(t, int64(0), *evidence.LogIndex) require.False(t, evidence.IntegratedTime.IsZero()) @@ -112,7 +110,6 @@ func TestVerifyKeySignedEnvelope(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.True(t, evidence.SignedWithKey) require.Empty(t, evidence.SignerIdentity) require.Nil(t, evidence.LogIndex) }) From 88b6ac5a4a80f9e1d95efe4a1c475cc849f4b8d5 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 12:22:52 +0000 Subject: [PATCH 10/17] no-mistakes(ci): Root cause: the failure came from this PR's test, not from infrastructure. CI exports KOSLI_GITHUB_TOKEN, and viper binds it to --github-token. Two cases in TestAttestInTotoCommandTestSuite/TestProvenanceWhenTheStatementLacksDetails ("records only the commit's SHA when its details cannot be found" and "has the server accept an artifact known only by its commit's SHA") expect that no commit lookup applies. With the ambient token set, the command called api.github.com for acme/widget, got a 404, and printed an extra "could not look up commit" warning. That warning broke the anchored regex and the golden. Invariant: tests of the "no lookup applies" path must not depend on whether the environment has a GitHub token. Both affected cases are in that one test function. The function's other cases pass --github-token explicitly, which overrides the env var. Fix: at the top of TestProvenanceWhenTheStatementLacksDetails, call suite.T().Setenv("KOSLI_GITHUB_TOKEN", ""). Viper's AllowEmptyEnv(false) treats an empty value as unset. Production code is unchanged. Verification: ran the suite method with KOSLI_GITHUB_TOKEN=bogus against the local server. All cases passed, including the two that failed in CI. I had to remove the "reads the commit's details from a local checkout" case temporarily, because go-git can't open this worktree's repo locally (worktreeconfig extension). That's an environment quirk that doesn't apply to CI's normal clone, and the file was restored afterwards. go vet and gofmt are clean --- cmd/kosli/attestInToto_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/kosli/attestInToto_test.go b/cmd/kosli/attestInToto_test.go index 385705e9d..07347707f 100644 --- a/cmd/kosli/attestInToto_test.go +++ b/cmd/kosli/attestInToto_test.go @@ -192,6 +192,8 @@ func (suite *AttestInTotoCommandTestSuite) TestProvenanceBuildsTheArtifactFromTh } 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") From 2103ac7562c495f6b9515993554737e96569bfe5 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 12:34:39 +0000 Subject: [PATCH 11/17] no-mistakes(ci): Root cause: the failure came from this PR's own test. The case TestAttestInTotoCommandTestSuite/TestProvenanceWhenTheStatementLacksDetails/"reads the commit's details from a local checkout that holds it" reads HEAD's commit message and author with the regex `"[^"]+"`. The commit from the previous round (b0660053) has a message containing escaped quotes (\"...\"). The JSON payload therefore held `\"`, the pattern couldn't match, and the case failed. The command's output was correct; the test broke on whatever text the latest commit happened to contain. Invariant: a regex over a JSON string the test doesn't control (the local checkout's commit message and author) must accept any valid JSON string, escapes included. The other `[^"]+` patterns in the file (lines 159 and 184) only match values from fixtures, so they're unaffected. Fix (test-only, cmd/kosli/attestInToto_test.go): added `const jsonString = "(?:[^"\\]|\\.)+"` and used it for the message and author fields in that case's goldenRegex. Production code is unchanged. Verification: - A throwaway Go test fed both patterns output with escaped quotes and a <-escaped author. The new pattern matched; the old one didn't, which reproduces the CI failure. - Ran TestAttestInTotoCommandTestSuite locally against the local server with KOSLI_GITHUB_TOKEN=bogus. All cases passed except this one, which can't run here: go-git can't open this worktree's repository ("does not support extension: worktreeconfig"). So the exact failing case hasn't been run end to end; that happens in CI's normal clone. - go vet and gofmt are clean. The change is not committed --- cmd/kosli/attestInToto_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/attestInToto_test.go b/cmd/kosli/attestInToto_test.go index 07347707f..f6500227b 100644 --- a/cmd/kosli/attestInToto_test.go +++ b/cmd/kosli/attestInToto_test.go @@ -234,11 +234,15 @@ func (suite *AttestInTotoCommandTestSuite) TestProvenanceWhenTheStatementLacksDe { 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": "[^"]+",\s+"author": "[^"]+",\s+"timestamp": \d+,\s+"branch": "main"`, + 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")) From 8dce16f7a808a0cdbd2285ebecee0adc9ac705ee Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:17:34 +0000 Subject: [PATCH 12/17] Check an SBOM attestation's subject even when no signer is pinned attest sbom returned before verification when no signer was pinned, and verification was the only place the subject was checked. An unsigned envelope about a different artifact was therefore recorded against --fingerprint. The statement says which artifact it describes whether or not it is signed, so the subject is now checked first, with the same error a signed attestation gets. A signed bundle given without a signer is still recorded as unverified, as before; the help text now says its signature is not checked. --- cmd/kosli/attestSbom.go | 26 +++++++++++++++++--------- cmd/kosli/attestSbom_test.go | 11 +++++++++++ internal/intoto/match.go | 9 +++++++++ internal/intoto/verify.go | 6 ++++-- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/cmd/kosli/attestSbom.go b/cmd/kosli/attestSbom.go index df0d0d5ee..a217edc88 100644 --- a/cmd/kosli/attestSbom.go +++ b/cmd/kosli/attestSbom.go @@ -56,13 +56,15 @@ 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. To verify its signature -first, pass ^--certificate-identity^ (or ^--certificate-identity-regexp^) with -^--certificate-oidc-issuer^, or ^--key^; an attestation about an artifact must -then list that artifact as its subject. 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^. +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. @@ -265,8 +267,9 @@ func (o *attestSbomOptions) loadSbom() ([]byte, error) { // 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 and is verified when a signer is pinned; evidence then says -// what was established, and is nil for a plain SBOM file. +// 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 err != nil { @@ -283,6 +286,11 @@ func (o *attestSbomOptions) unwrap(content []byte) (document []byte, evidence *i 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 } diff --git a/cmd/kosli/attestSbom_test.go b/cmd/kosli/attestSbom_test.go index 8062e70be..c67aae5df 100644 --- a/cmd/kosli/attestSbom_test.go +++ b/cmd/kosli/attestSbom_test.go @@ -118,6 +118,17 @@ func (suite *AttestSbomCommandTestSuite) TestAttestSbomUnwrapsAnAttestation() { 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", diff --git a/internal/intoto/match.go b/internal/intoto/match.go index c9956f1df..9a7924d59 100644 --- a/internal/intoto/match.go +++ b/internal/intoto/match.go @@ -120,6 +120,15 @@ func (a *Attestation) HasSubject(digest string) bool { 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) diff --git a/internal/intoto/verify.go b/internal/intoto/verify.go index 8aa312105..95a64ab0f 100644 --- a/internal/intoto/verify.go +++ b/internal/intoto/verify.go @@ -107,8 +107,10 @@ func NewVerifier(policy Policy, trust *TrustRoots) *Verifier { // the policy pins. An empty digest checks the signature alone, for an // attestation recorded on a trail rather than an artifact. func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { - if digest != "" && !a.HasSubject(digest) { - return nil, fmt.Errorf("%s does not list the artifact (sha256:%s) as its subject", a.Describe(), normaliseDigest(digest)) + if digest != "" { + if err := a.RequireSubject(digest); err != nil { + return nil, err + } } rawSum := sha256.Sum256(a.Raw) evidence := &Evidence{RawSHA256: hex.EncodeToString(rawSum[:])} From 7719a9ea5651a955d305155ed6980fe89148d2da Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:18:27 +0000 Subject: [PATCH 13/17] Point attest sbom at attest in-toto for a file of several attestations An .intoto.jsonl file with several lines failed the single-envelope probe, was taken for a plain SBOM, and was reported as "not valid JSON" by the SBOM parser. ReadEnvelope now says the content holds several documents, and attest sbom names the command that picks the SBOM from such a file. --- cmd/kosli/attestSbom.go | 4 ++++ cmd/kosli/attestSbom_test.go | 6 ++++++ internal/intoto/read.go | 10 +++++++++- internal/intoto/read_test.go | 8 ++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cmd/kosli/attestSbom.go b/cmd/kosli/attestSbom.go index a217edc88..e85e4bbf7 100644 --- a/cmd/kosli/attestSbom.go +++ b/cmd/kosli/attestSbom.go @@ -2,6 +2,7 @@ package main import ( "crypto/sha256" + "errors" "fmt" "io" "os" @@ -272,6 +273,9 @@ func (o *attestSbomOptions) loadSbom() ([]byte, error) { // 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) } diff --git a/cmd/kosli/attestSbom_test.go b/cmd/kosli/attestSbom_test.go index c67aae5df..a2345f86c 100644 --- a/cmd/kosli/attestSbom_test.go +++ b/cmd/kosli/attestSbom_test.go @@ -135,6 +135,12 @@ func (suite *AttestSbomCommandTestSuite) TestAttestSbomUnwrapsAnAttestation() { 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", diff --git a/internal/intoto/read.go b/internal/intoto/read.go index 4f2c2e556..34d455ebf 100644 --- a/internal/intoto/read.go +++ b/internal/intoto/read.go @@ -87,16 +87,24 @@ func Read(content []byte) ([]*Attestation, error) { 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. +// 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 == "" { diff --git a/internal/intoto/read_test.go b/internal/intoto/read_test.go index 65dd9072b..7bb82b452 100644 --- a/internal/intoto/read_test.go +++ b/internal/intoto/read_test.go @@ -160,6 +160,14 @@ func TestReadEnvelope(t *testing.T) { } }) + 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) From 290b885bd455562fe620477e9b2d487870124f04 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:19:45 +0000 Subject: [PATCH 14/17] Say that a GitHub Enterprise commit lookup needs --github-base-url With --repo-provider github for a GitHub Enterprise host, the lookup still went to github.com unless --github-base-url was also given, and fell back to a warning. The help for --repo-provider, --github-base-url and the command now says so. The API URL is deliberately not derived from the statement's host, so the token only goes to a server the user chose. --- cmd/kosli/attestInToto.go | 8 +++++--- cmd/kosli/root.go | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/kosli/attestInToto.go b/cmd/kosli/attestInToto.go index bb3221cb2..0576be2b0 100644 --- a/cmd/kosli/attestInToto.go +++ b/cmd/kosli/attestInToto.go @@ -110,8 +110,10 @@ 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 build URL or commit URL the statement does not -carry can be given with ^--build-url^ and ^--commit-url^. +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 = ` @@ -216,7 +218,7 @@ func newAttestInTotoCmd(out io.Writer) *cobra.Command { 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", "", githubBaseURLFlag) + 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) diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index 1f4d55bf9..832933e54 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -349,9 +349,10 @@ Paths the list already matches stay excluded whatever is later added there, so k 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 + 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" From 51952383f4e7d0cb024ebb102910ba5187d83e9d Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:22:33 +0000 Subject: [PATCH 15/17] Record nothing when any attestation in the file fails verification When a file held a provenance from the wrong signer and an unsigned one, and --allow-unsigned was set, the unsigned one was recorded and the bad signature only produced a warning. A signature that does not verify is a stronger signal than a missing one, so attest in-toto now fails and records nothing when any candidate fails verification, by another signer or after being changed, even if another candidate would qualify. This matches "a wrong signer identity or tampered payload fails verification and records nothing". Candidates that are merely unsigned are still passed over with a warning, and the rest of the choice is unchanged: verified first, then the newest. --- cmd/kosli/attestInToto.go | 4 +- cmd/kosli/attestInToto_test.go | 6 +++ internal/intoto/find.go | 25 +++++++--- internal/intoto/fixtures_test.go | 4 ++ internal/intoto/testdata/README.md | 1 + .../wrong-signer-and-unsigned.intoto.jsonl | 2 + internal/intoto/verify.go | 8 +++- internal/intoto/verify_test.go | 48 +++++++++++++++++++ 8 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 internal/intoto/testdata/wrong-signer-and-unsigned.intoto.jsonl diff --git a/cmd/kosli/attestInToto.go b/cmd/kosli/attestInToto.go index 0576be2b0..abf2d739e 100644 --- a/cmd/kosli/attestInToto.go +++ b/cmd/kosli/attestInToto.go @@ -99,7 +99,9 @@ with Sigstore against a signer you pin: ^--certificate-identity^ (or 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. +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. 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. diff --git a/cmd/kosli/attestInToto_test.go b/cmd/kosli/attestInToto_test.go index f6500227b..5291334a9 100644 --- a/cmd/kosli/attestInToto_test.go +++ b/cmd/kosli/attestInToto_test.go @@ -331,6 +331,12 @@ func (suite *AttestInTotoCommandTestSuite) TestRefusesWhatItCannotTrust() { 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 statement changed after signing", diff --git a/internal/intoto/find.go b/internal/intoto/find.go index 470908067..284793e83 100644 --- a/internal/intoto/find.go +++ b/internal/intoto/find.go @@ -44,14 +44,18 @@ type Found struct { *Attestation Evidence *Evidence // Rejected holds why other attestations of the same kind about the same - // artifact were not chosen, so a caller can report them. + // 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. +// trusted time, then the one read last. If any fails verification, Find +// returns that and chooses none: a bad signature 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 { @@ -63,14 +67,23 @@ func Find(source Source, kind PredicateKind, digest string, verifier *Verifier) } var accepted []*Found - var rejected []error + var rejected, failed []error for _, candidate := range candidates { evidence, err := verifier.Verify(candidate, digest) - if err != nil { + switch { + case errors.Is(err, ErrFailedVerification): + failed = append(failed, err) + case err != nil: rejected = append(rejected, err) - continue + default: + accepted = append(accepted, &Found{Attestation: candidate, Evidence: evidence}) } - accepted = append(accepted, &Found{Attestation: candidate, Evidence: evidence}) + } + if len(failed) == 1 { + return nil, fmt.Errorf("%s: %w", source, failed[0]) + } + if len(failed) > 1 { + return nil, fmt.Errorf("%s: %d %s attestations %w: %w", source, len(failed), kind, ErrFailedVerification, joinErrors(failed)) } if len(accepted) == 0 { if len(rejected) == 1 { diff --git a/internal/intoto/fixtures_test.go b/internal/intoto/fixtures_test.go index 52dda650e..26fbf4a4a 100644 --- a/internal/intoto/fixtures_test.go +++ b/internal/intoto/fixtures_test.go @@ -278,6 +278,10 @@ func TestRegenerateFixtures(t *testing.T) { 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) diff --git a/internal/intoto/testdata/README.md b/internal/intoto/testdata/README.md index 612d1bd2c..e2250ff69 100644 --- a/internal/intoto/testdata/README.md +++ b/internal/intoto/testdata/README.md @@ -36,5 +36,6 @@ Sigstore signature is checked against the time Rekor logged it. | `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/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/verify.go b/internal/intoto/verify.go index 95a64ab0f..8160373f9 100644 --- a/internal/intoto/verify.go +++ b/internal/intoto/verify.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "errors" "fmt" "net/url" "os" @@ -92,6 +93,11 @@ func (e *Evidence) Time() time.Time { } // Verifier checks attestations against a policy. +// ErrFailedVerification marks an attestation whose signature was checked and +// did not verify against the policy, such as one by another signer or one +// changed after signing, as distinct from one that is unsigned. +var ErrFailedVerification = errors.New("failed verification") + type Verifier struct { policy Policy trust *TrustRoots @@ -127,7 +133,7 @@ func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { result, entity, err := v.verifySignature(a, digest) if err != nil { - return nil, fmt.Errorf("%s failed verification: %w", a.Describe(), err) + return nil, fmt.Errorf("%s %w: %w", a.Describe(), ErrFailedVerification, err) } evidence.Verified = true diff --git a/internal/intoto/verify_test.go b/internal/intoto/verify_test.go index 0c01acc31..e1b13434c 100644 --- a/internal/intoto/verify_test.go +++ b/internal/intoto/verify_test.go @@ -1,6 +1,7 @@ package intoto import ( + "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -183,6 +184,21 @@ func TestPolicyValidate(t *testing.T) { }) } +// 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))) @@ -212,6 +228,38 @@ func TestFind(t *testing.T) { 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 failed verification: attestation 1 failed verification: ") + require.ErrorContains(t, err, "; attestation 2 failed verification: ") + }) + + 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") From 5aa2ad0bd08c49aa9228c87c4b4470d5f03cc5a0 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 15:29:48 +0000 Subject: [PATCH 16/17] no-mistakes(document): Restore Verifier doc comment in intoto --- internal/intoto/verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/intoto/verify.go b/internal/intoto/verify.go index 8160373f9..a3fa3fc78 100644 --- a/internal/intoto/verify.go +++ b/internal/intoto/verify.go @@ -92,12 +92,12 @@ func (e *Evidence) Time() time.Time { return e.SignedTime } -// Verifier checks attestations against a policy. // ErrFailedVerification marks an attestation whose signature was checked and // did not verify against the policy, such as one by another signer or one // changed after signing, as distinct from one that is unsigned. var ErrFailedVerification = errors.New("failed verification") +// Verifier checks attestations against a policy. type Verifier struct { policy Policy trust *TrustRoots From 8cda42faee31a6e4d15342da4e7ca0f5891cbe74 Mon Sep 17 00:00:00 2001 From: Steve Tooke Date: Wed, 11 Nov 2026 18:13:06 +0000 Subject: [PATCH 17/17] Tell a signature that cannot be checked apart from a bad one Every error on the way to checking a signature was reported as "failed verification", so a trust root that could not be fetched, a certificate issuer with no trust root, a key file that could not be read, or signer flags that do not fit how the attestation was signed (identity flags on a key-signed bundle or DSSE envelope, --key on a certificate) read as a forged attestation. Setting up the check and making it are now separate steps. Anything that stops the check being made is ErrCannotVerify, reported as "could not be verified"; only a signature, identity, log entry or timestamp that the check rejects is ErrFailedVerification. attest in-toto still records nothing when any candidate is either, and the help text says which error means what. With several such candidates the message no longer repeats "failed verification". --- cmd/kosli/attestInToto.go | 4 +- cmd/kosli/attestInToto_test.go | 6 +++ internal/intoto/find.go | 41 +++++++++------ internal/intoto/verify.go | 88 +++++++++++++++++++++---------- internal/intoto/verify_test.go | 95 ++++++++++++++++++++++++++++++++-- 5 files changed, 186 insertions(+), 48 deletions(-) diff --git a/cmd/kosli/attestInToto.go b/cmd/kosli/attestInToto.go index abf2d739e..478fc81d2 100644 --- a/cmd/kosli/attestInToto.go +++ b/cmd/kosli/attestInToto.go @@ -101,7 +101,9 @@ of GitHub's own instance, which signs attestations for private repositories, are 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. +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. diff --git a/cmd/kosli/attestInToto_test.go b/cmd/kosli/attestInToto_test.go index 5291334a9..f15aa1ac6 100644 --- a/cmd/kosli/attestInToto_test.go +++ b/cmd/kosli/attestInToto_test.go @@ -337,6 +337,12 @@ func (suite *AttestInTotoCommandTestSuite) TestRefusesWhatItCannotTrust() { 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", diff --git a/internal/intoto/find.go b/internal/intoto/find.go index 284793e83..fddfb67b0 100644 --- a/internal/intoto/find.go +++ b/internal/intoto/find.go @@ -52,10 +52,11 @@ type Found struct { // 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 fails verification, Find -// returns that and chooses none: a bad signature is a stronger signal than a -// missing one, so an unsigned or verified alternative is not recorded in its -// place. +// 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 { @@ -67,29 +68,29 @@ func Find(source Source, kind PredicateKind, digest string, verifier *Verifier) } var accepted []*Found - var rejected, failed []error + var rejected, unverifiable []error for _, candidate := range candidates { evidence, err := verifier.Verify(candidate, digest) switch { - case errors.Is(err, ErrFailedVerification): - failed = append(failed, err) + 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(failed) == 1 { - return nil, fmt.Errorf("%s: %w", source, failed[0]) + if len(unverifiable) == 1 { + return nil, fmt.Errorf("%s: %w", source, unverifiable[0]) } - if len(failed) > 1 { - return nil, fmt.Errorf("%s: %d %s attestations %w: %w", source, len(failed), kind, ErrFailedVerification, joinErrors(failed)) + 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, joinErrors(rejected)) + 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 { @@ -104,10 +105,18 @@ func Find(source Source, kind PredicateKind, digest string, verifier *Verifier) return chosen, nil } -func joinErrors(errs []error) error { - messages := make([]string, len(errs)) - for i, err := range errs { +// 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 errors.New(strings.Join(messages, "; ")) + return strings.Join(messages, "; ") +} + +func (e joinedErrors) Unwrap() []error { + return e } diff --git a/internal/intoto/verify.go b/internal/intoto/verify.go index a3fa3fc78..6bdf84dce 100644 --- a/internal/intoto/verify.go +++ b/internal/intoto/verify.go @@ -3,6 +3,7 @@ package intoto import ( "crypto" "crypto/sha256" + "crypto/x509" "encoding/base64" "encoding/hex" "errors" @@ -92,11 +93,19 @@ func (e *Evidence) Time() time.Time { return e.SignedTime } -// ErrFailedVerification marks an attestation whose signature was checked and -// did not verify against the policy, such as one by another signer or one -// changed after signing, as distinct from one that is unsigned. +// 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 @@ -111,7 +120,9 @@ func NewVerifier(policy Policy, trust *TrustRoots) *Verifier { // 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. +// 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 { @@ -131,7 +142,11 @@ func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { 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()) } - result, entity, err := v.verifySignature(a, digest) + 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) } @@ -153,30 +168,33 @@ func (v *Verifier) Verify(a *Attestation, digest string) (*Evidence, error) { return evidence, nil } -func (v *Verifier) verifySignature(a *Attestation, digest string) (*verify.VerificationResult, *bundle.Bundle, error) { +// 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, fmt.Errorf("the artifact digest is not hexadecimal: %w", err) + 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.verifyWithKey(a, artifactPolicy) + return v.keyCheck(a, artifactPolicy) } if a.bundle == nil { - return nil, nil, fmt.Errorf("a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope with --key") + 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") } - verificationContent, err := a.bundle.VerificationContent() + leaf, err := signingCertificate(a.bundle) if err != nil { - return nil, nil, err + return nil, nil, verify.PolicyBuilder{}, err } - leaf := verificationContent.Certificate() if leaf == nil { - return nil, nil, fmt.Errorf("it is signed with a key, not a certificate. Verify it with --key") + 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 { @@ -184,24 +202,23 @@ func (v *Verifier) verifySignature(a *Attestation, digest string) (*verify.Verif } trustedMaterial, opts, err := v.trust.forCertificate(issuerOrg) if err != nil { - return nil, nil, err + 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, err + return nil, nil, verify.PolicyBuilder{}, err } verifier, err := verify.NewVerifier(trustedMaterial, opts...) if err != nil { - return nil, nil, err + return nil, nil, verify.PolicyBuilder{}, err } - result, err := verifier.Verify(a.bundle, verify.NewPolicy(artifactPolicy, verify.WithCertificateIdentity(identity))) - return result, a.bundle, err + return verifier, a.bundle, verify.NewPolicy(artifactPolicy, verify.WithCertificateIdentity(identity)), nil } -func (v *Verifier) verifyWithKey(a *Attestation, artifactPolicy verify.ArtifactPolicyOption) (*verify.VerificationResult, *bundle.Bundle, error) { +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, err + return nil, nil, verify.PolicyBuilder{}, err } keyMaterial := root.NewTrustedPublicKeyMaterial(func(string) (root.TimeConstrainedVerifier, error) { return key, nil @@ -211,7 +228,15 @@ func (v *Verifier) verifyWithKey(a *Attestation, artifactPolicy verify.ArtifactP if entity == nil { entity, err = envelopeAsBundle(a) if err != nil { - return nil, nil, err + 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") } } @@ -219,11 +244,11 @@ func (v *Verifier) verifyWithKey(a *Attestation, artifactPolicy verify.ArtifactP var opts []verify.VerifierOption tlogEntries, err := entity.TlogEntries() if err != nil { - return nil, nil, err + return nil, nil, verify.PolicyBuilder{}, err } timestamps, err := entity.Timestamps() if err != nil { - return nil, nil, err + return nil, nil, verify.PolicyBuilder{}, err } switch { case len(tlogEntries) > 0: @@ -236,17 +261,26 @@ func (v *Verifier) verifyWithKey(a *Attestation, artifactPolicy verify.ArtifactP if len(tlogEntries) > 0 || len(timestamps) > 0 { trustRoot, err := v.trust.forKey() if err != nil { - return nil, nil, err + return nil, nil, verify.PolicyBuilder{}, err } trustedMaterial = root.TrustedMaterialCollection{trustRoot, keyMaterial} } verifier, err := verify.NewVerifier(trustedMaterial, opts...) if err != nil { - return nil, nil, err + 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 } - result, err := verifier.Verify(entity, verify.NewPolicy(artifactPolicy, verify.WithKey())) - return result, entity, err + return content.Certificate(), nil } // envelopeAsBundle puts a bare DSSE envelope in a bundle whose verification diff --git a/internal/intoto/verify_test.go b/internal/intoto/verify_test.go index e1b13434c..392204a6d 100644 --- a/internal/intoto/verify_test.go +++ b/internal/intoto/verify_test.go @@ -6,10 +6,17 @@ import ( "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" @@ -75,6 +82,8 @@ func TestVerifySignedBundle(t *testing.T) { 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) }) @@ -87,6 +96,8 @@ func TestVerifySignedBundle(t *testing.T) { 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") }) @@ -100,9 +111,11 @@ func TestVerifySignedBundle(t *testing.T) { 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("refuses a certificate signature checked with --key", func(t *testing.T) { + 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.ErrorContains(t, err, "expected key signature, not certificate") + 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") }) } @@ -124,20 +137,85 @@ func TestVerifyKeySignedEnvelope(t *testing.T) { 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.EqualError(t, err, "the attestation failed verification: a DSSE envelope carries no certificate to check --certificate-identity against. Supply the Sigstore bundle instead, or verify the envelope with --key") + 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")) @@ -247,10 +325,19 @@ func TestFind(t *testing.T) { _, err := Find(BundleFile{Path: path}, KindProvenance, artifactDigest, NewVerifier(pinnedSigner, fixtureTrust(t))) require.ErrorIs(t, err, ErrFailedVerification) - require.ErrorContains(t, err, ": 2 provenance attestations failed verification: attestation 1 failed verification: ") + 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"))