diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd5695abc..5aad3e925 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,3 +2,6 @@ pkg/cmd/codespace/ @cli/codespaces internal/codespaces/ @cli/codespaces + +# Limit Package Security team ownership to the attestation command package +pkg/cmd/attestation/ @cli/package-security diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 13b598d52..7284a9e08 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,4 +1,4 @@ -name: Tests +name: Unit and Integration Tests on: [push, pull_request] permissions: @@ -37,3 +37,27 @@ jobs: - name: Build run: go build -v ./cmd/gh + + integration-tests: + env: + GH_TOKEN: ${{ github.token }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Set up Go 1.21 + uses: actions/setup-go@v5 + with: + go-version: 1.21 + + - name: Check out code + uses: actions/checkout@v4 + + - name: Build executable + run: make + + - name: Run attestation command integration Tests + run: ./test/integration/attestation-cmd/download-and-verify-package-attestation.sh diff --git a/go.mod b/go.mod index 3d432895c..e419c3224 100644 --- a/go.mod +++ b/go.mod @@ -14,14 +14,17 @@ require ( github.com/cli/safeexec v1.0.1 github.com/cpuguy83/go-md2man/v2 v2.0.4 github.com/creack/pty v1.1.21 + github.com/distribution/reference v0.5.0 github.com/gabriel-vasile/mimetype v1.4.3 github.com/gdamore/tcell/v2 v2.5.4 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 + github.com/google/go-containerregistry v0.19.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/gorilla/websocket v1.4.2 + github.com/gorilla/websocket v1.5.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.1.3 + github.com/in-toto/in-toto-golang v0.9.0 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.13 @@ -29,18 +32,20 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/microsoft/dev-tunnels v0.0.25 github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 - github.com/opentracing/opentracing-go v1.1.0 + github.com/opentracing/opentracing-go v1.2.0 github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 - github.com/spf13/cobra v1.6.1 + github.com/sigstore/protobuf-specs v0.3.0 + github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 github.com/zalando/go-keyring v0.2.4 - golang.org/x/crypto v0.17.0 - golang.org/x/sync v0.1.0 - golang.org/x/term v0.15.0 + golang.org/x/crypto v0.19.0 + golang.org/x/sync v0.6.0 + golang.org/x/term v0.17.0 golang.org/x/text v0.14.0 - google.golang.org/grpc v1.56.3 + google.golang.org/grpc v1.61.0 google.golang.org/protobuf v1.33.0 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 @@ -49,41 +54,113 @@ require ( require ( github.com/alecthomas/chroma v0.10.0 // indirect github.com/alessio/shellescape v1.4.2 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect github.com/cli/browser v1.3.0 // indirect github.com/cli/shurcooL-graphql v0.0.4 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect github.com/danieljoos/wincred v1.2.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect - github.com/fatih/color v1.7.0 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.7+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/fatih/color v1.14.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect + github.com/go-chi/chi v4.1.2+incompatible // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.22.0 // indirect + github.com/go-openapi/errors v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/loads v0.21.5 // indirect + github.com/go-openapi/runtime v0.27.1 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/strfmt v0.22.0 // indirect + github.com/go-openapi/swag v0.22.9 // indirect + github.com/go-openapi/validate v0.22.6 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/certificate-transparency-go v1.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.13 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/microcosm-cc/bluemonday v1.0.26 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.13.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc5 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rodaine/table v1.0.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sassoftware/relic v7.2.1+incompatible // indirect + github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect + github.com/sigstore/rekor v1.3.5 // indirect + github.com/sigstore/sigstore v1.8.1 // indirect + github.com/sigstore/timestamp-authority v1.2.2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/viper v1.18.2 // indirect github.com/stretchr/objx v0.5.0 // 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.0.0-20240222081530-454b12158917 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/yuin/goldmark v1.5.2 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect - golang.org/x/net v0.17.0 // indirect + go.mongodb.org/mongo-driver v1.13.1 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.18.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + k8s.io/klog/v2 v2.120.0 // indirect ) diff --git a/go.sum b/go.sum index c05d673cd..ac71cdba3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,31 @@ +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM= +cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= @@ -8,14 +34,54 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +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 v1.49.21 h1:Rl8KW6HqkwzhATwvXhyr7vD4JFUMi7oXGAw9SrxxIFY= +github.com/aws/aws-sdk-go v1.49.21/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= +github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= +github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.9 h1:W9PbZAZAEcelhhjb7KuwUtf+Lbc+i7ByYJRuWLlnxyQ= +github.com/aws/aws-sdk-go-v2/service/kms v1.27.9/go.mod h1:2tFmR7fQnOdQlM2ZCEPpFnBIQD1U8wmXmduBgZbOag0= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +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/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= @@ -32,70 +98,199 @@ github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= +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/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h1:vU+EP9ZuFUCYE0NYLwTSob+3LNEJATzNfP/DC7SWGWI= +github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +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.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k= github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/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 v0.22.0 h1:wQ/d07nf78HNj4u+KiSY0sT234IAyePPbMgpUjUJQR0= +github.com/go-openapi/analysis v0.22.0/go.mod h1:acDnkkCI2QxIo8sSIPgmp1wUlRohV7vfGtAIVae73b0= +github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY= +github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0= +github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8= +github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqvJYto= +github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI= +github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo= +github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= +github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic= +github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +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.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 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/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= +github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= +github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4= +github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5FjV2mCWI= +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= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +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 v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/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.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +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.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +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.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= +github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8= github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= +github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +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.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8= +github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo= +github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -110,6 +305,9 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -118,6 +316,11 @@ github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3r github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA= github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ= +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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= @@ -128,12 +331,33 @@ github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0Fr github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0= github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -142,28 +366,95 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ= github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.1 h1:O5s8ewCgq5QYNpv45dK4u6IpBmDM9RIcsbf/G1uXepQ= +github.com/sassoftware/relic/v7 v7.6.1/go.mod h1:NxwtWxWxlUa9as2qZi635Ye6bBT/tGnMALLq7dSfOOU= +github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= +github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +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/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46DPtb8k793jiecUEhaX9ixoIBt41HEGU= github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/sigstore/protobuf-specs v0.3.0 h1:E49qS++llp4psM+3NNVEb+C4AD422bT9VkOQIPrNLpA= +github.com/sigstore/protobuf-specs v0.3.0/go.mod h1:ynKzXpqr3dUj2Xk9O/5ZUhjnpi0F53DNi5AdH6pS3jc= +github.com/sigstore/rekor v1.3.5 h1:QoVXcS7NppKY+rpbEFVHr4evGDZBBSh65X0g8PXoUkQ= +github.com/sigstore/rekor v1.3.5/go.mod h1:CWqOk/fmnPwORQmm7SyDgB54GTJizqobbZ7yOP1lvw8= +github.com/sigstore/sigstore v1.8.1 h1:mAVposMb14oplk2h/bayPmIVdzbq2IhCgy4g6R0ZSjo= +github.com/sigstore/sigstore v1.8.1/go.mod h1:02SL1158BSj15bZyOFz7m+/nJzLZfFd9A8ab3Kz7w/E= +github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc h1:S8mCkKxbnn38DQz41jnPyaSKZYviGT7wMLM+7iHOp3I= +github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc/go.mod h1:yODm8pZ33BSpmezTcvvwzgHZgDxbqB6hBAg1izfG+EQ= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.1 h1:rEDdUefulkIQaMJyzLwtgPDLNXBIltBABiFYfb0YmgQ= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.1/go.mod h1:RCdYCc1IxCYWzh2IdzdA6Yf7JIY0cMRqH08fpQYechw= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.1 h1:DvRWG99QGWZC5mp42SEde2Xke/Q384Idnj2da7yB+Mk= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.1/go.mod h1:s13mo3a0UCQS3+PAUUZfvKe48sMDMsHk2GE1b2YfPcU= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.1 h1:lwdRsJv1UbBemuk7w5YfXAQilQxMoFevrzamdPbG0wY= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.1/go.mod h1:2OaSQ80EcdyVRSQ3T4d1lsc6Scopblsiq8U2AEk5K1A= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.1 h1:9Ki0qudKpc1FQdef7xHO2bkLyTuw+qNUpWRzjBEmF4c= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.1/go.mod h1:nhIgyu4YwwNgalIwTGsoAzam16jjAn3ADRSWKbWPwGI= +github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE= +github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +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.0.0-20240222081530-454b12158917 h1:Ov8+IAeR7pivNDC0Cd25MyyaCR3WPlGBED4wNxIFQ8s= +github.com/theupdateframework/go-tuf/v2 v2.0.0-20240222081530-454b12158917/go.mod h1:+gWwqe1pk4nvGeOKosGJqPgD+N/kbD9M0QVLL9TGIYU= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +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/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= @@ -172,47 +463,85 @@ github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18W github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= +go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.step.sm/crypto v0.43.0 h1:siTS/iiqaX4qBUeTxVyag5I2rijuKOMDkXSnrKcei7s= +go.step.sm/crypto v0.43.0/go.mod h1:iKrtuRbFlqimEG/+fWSu7kcZzl4Bd/+w5xkuqA5OSic= +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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -220,21 +549,40 @@ golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/api v0.160.0 h1:SEspjXHVqE1m5a1fRy8JFB+5jSu+V0GEDKDghF3ttO4= +google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U= +gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +k8s.io/klog/v2 v2.120.0 h1:z+q5mfovBj1fKFxiRzsa2DsJLPIVMk/KFL81LMOfK+8= +k8s.io/klog/v2 v2.120.0/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= +software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go new file mode 100644 index 000000000..2b96a51fc --- /dev/null +++ b/pkg/cmd/attestation/api/attestation.go @@ -0,0 +1,33 @@ +package api + +import ( + "fmt" + + "github.com/sigstore/sigstore-go/pkg/bundle" +) + +const ( + GetAttestationByRepoAndSubjectDigestPath = "repos/%s/attestations/%s" + GetAttestationByOwnerAndSubjectDigestPath = "orgs/%s/attestations/%s" +) + +type ErrNoAttestations struct { + name string + digest string +} + +func (e ErrNoAttestations) Error() string { + return fmt.Sprintf("no attestations found for digest %s in %s", e.name, e.digest) +} + +func newErrNoAttestations(name, digest string) ErrNoAttestations { + return ErrNoAttestations{name, digest} +} + +type Attestation struct { + Bundle *bundle.ProtobufBundle `json:"bundle"` +} + +type AttestationsResponse struct { + Attestations []*Attestation `json:"attestations"` +} diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go new file mode 100644 index 000000000..b0cafa6e9 --- /dev/null +++ b/pkg/cmd/attestation/api/client.go @@ -0,0 +1,104 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/cli/cli/v2/api" + ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/go-gh/v2/pkg/auth" +) + +const ( + DefaultLimit = 30 + maxLimitForFlag = 1000 + maxLimitForFetch = 100 +) + +type apiClient interface { + RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) +} + +type Client interface { + GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) + GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) +} + +type LiveClient struct { + api apiClient + host string + logger *ioconfig.Handler +} + +func NewLiveClient(hc *http.Client, l *ioconfig.Handler) *LiveClient { + host, _ := auth.DefaultHost() + + return &LiveClient{ + api: api.NewClientFromHTTP(hc), + host: strings.TrimSuffix(host, "/"), + logger: l, + } +} + +func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string { + repo = strings.Trim(repo, "/") + return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest) +} + +// GetByRepoAndDigest fetches the attestation by repo and digest +func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { + url := c.BuildRepoAndDigestURL(repo, digest) + return c.getAttestations(url, repo, digest, limit) +} + +func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string { + owner = strings.Trim(owner, "/") + return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest) +} + +// GetByOwnerAndDigest fetches attestation by owner and digest +func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { + url := c.BuildOwnerAndDigestURL(owner, digest) + return c.getAttestations(url, owner, digest, limit) +} + +func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) { + c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) + + perPage := limit + if perPage <= 0 || perPage > maxLimitForFlag { + return nil, fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag) + } + + if perPage > maxLimitForFetch { + perPage = maxLimitForFetch + } + + // ref: https://github.com/cli/go-gh/blob/d32c104a9a25c9de3d7c7b07a43ae0091441c858/example_gh_test.go#L96 + url = fmt.Sprintf("%s?per_page=%d", url, perPage) + + var attestations []*Attestation + var resp AttestationsResponse + var err error + // if no attestation or less than limit, then keep fetching + for url != "" && len(attestations) < limit { + url, err = c.api.RESTWithNext(c.host, http.MethodGet, url, nil, &resp) + if err != nil { + return nil, err + } + + attestations = append(attestations, resp.Attestations...) + } + + if len(attestations) == 0 { + return nil, newErrNoAttestations(name, digest) + } + + if len(attestations) > limit { + return attestations[:limit], nil + } + + return attestations, nil +} diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go new file mode 100644 index 000000000..7044a1cc5 --- /dev/null +++ b/pkg/cmd/attestation/api/client_test.go @@ -0,0 +1,174 @@ +package api + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + + "github.com/stretchr/testify/require" +) + +const ( + testRepo = "github/example" + testOwner = "github" + testDigest = "sha256:12313213" +) + +func NewClientWithMockGHClient(hasNextPage bool) Client { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + l := io.NewTestHandler() + + if hasNextPage { + return &LiveClient{ + api: mockAPIClient{ + OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage, + }, + logger: l, + } + } + + return &LiveClient{ + api: mockAPIClient{ + OnRESTWithNext: fetcher.OnRESTSuccess, + }, + logger: l, + } +} + +func TestGetURL(t *testing.T) { + c := LiveClient{} + + testData := []struct { + repo string + digest string + expected string + }{ + {repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, + {repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"}, + } + + for _, data := range testData { + s := c.BuildRepoAndDigestURL(data.repo, data.digest) + require.Equal(t, data.expected, s) + } +} + +func TestGetByDigest(t *testing.T) { + c := NewClientWithMockGHClient(false) + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + require.NoError(t, err) + + require.Equal(t, 5, len(attestations)) + bundle := (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + require.NoError(t, err) + + require.Equal(t, 5, len(attestations)) + bundle = (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigestGreaterThanLimit(t *testing.T) { + c := NewClientWithMockGHClient(false) + + limit := 3 + // The method should return five results when the limit is not set + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit) + require.NoError(t, err) + + require.Equal(t, 3, len(attestations)) + bundle := (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit) + require.NoError(t, err) + + require.Equal(t, len(attestations), limit) + bundle = (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigestWithNextPage(t *testing.T) { + c := NewClientWithMockGHClient(true) + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + require.NoError(t, err) + + require.Equal(t, len(attestations), 10) + bundle := (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + require.NoError(t, err) + + require.Equal(t, len(attestations), 10) + bundle = (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigestGreaterThanLimitWithNextPage(t *testing.T) { + c := NewClientWithMockGHClient(true) + + limit := 7 + // The method should return five results when the limit is not set + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit) + require.NoError(t, err) + + require.Equal(t, len(attestations), limit) + bundle := (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit) + require.NoError(t, err) + + require.Equal(t, len(attestations), limit) + bundle = (attestations)[0].Bundle + require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1") +} + +func TestGetByDigest_NoAttestationsFound(t *testing.T) { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + + c := LiveClient{ + api: mockAPIClient{ + OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations, + }, + logger: io.NewTestHandler(), + } + + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + require.Error(t, err) + require.IsType(t, ErrNoAttestations{}, err) + require.Nil(t, attestations) + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + require.Error(t, err) + require.IsType(t, ErrNoAttestations{}, err) + require.Nil(t, attestations) +} + +func TestGetByDigest_Error(t *testing.T) { + fetcher := mockDataGenerator{ + NumAttestations: 5, + } + + c := LiveClient{ + api: mockAPIClient{ + OnRESTWithNext: fetcher.OnRESTWithNextError, + }, + logger: io.NewTestHandler(), + } + + attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit) + require.Error(t, err) + require.Nil(t, attestations) + + attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit) + require.Error(t, err) + require.Nil(t, attestations) +} diff --git a/pkg/cmd/attestation/api/mock_apiClient_test.go b/pkg/cmd/attestation/api/mock_apiClient_test.go new file mode 100644 index 000000000..1d4f61cd9 --- /dev/null +++ b/pkg/cmd/attestation/api/mock_apiClient_test.go @@ -0,0 +1,89 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" +) + +type mockAPIClient struct { + OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error) +} + +func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + return m.OnRESTWithNext(hostname, method, p, body, data) +} + +type mockDataGenerator struct { + NumAttestations int +} + +func (m mockDataGenerator) OnRESTSuccess(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false) +} + +func (m mockDataGenerator) OnRESTSuccessWithNextPage(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + // if path doesn't contain after, it means first time hitting the mock server + // so return the first page and return the link header in the response + if !strings.Contains(p, "after") { + return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, true) + } + + // if path contain after, it means second time hitting the mock server and will not return the link header + return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false) +} + +func (m mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) (string, error) { + atts := make([]*Attestation, m.NumAttestations) + for j := 0; j < m.NumAttestations; j++ { + att := makeTestAttestation() + atts[j] = &att + } + + resp := AttestationsResponse{ + Attestations: atts, + } + + // // Convert the attestations to JSON + b, err := json.Marshal(resp) + if err != nil { + return "", err + } + + err = json.Unmarshal(b, &data) + if err != nil { + return "", err + } + + if hasNext { + // return a link header with the next page + return fmt.Sprintf("<%s&after=2>; rel=\"next\"", p), nil + } + + return "", nil +} + +func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + resp := AttestationsResponse{ + Attestations: make([]*Attestation, 0), + } + + // // Convert the attestations to JSON + b, err := json.Marshal(resp) + if err != nil { + return "", err + } + + err = json.Unmarshal(b, &data) + if err != nil { + return "", err + } + + return "", nil +} + +func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) { + return "", errors.New("failed to get attestations") +} diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go new file mode 100644 index 000000000..96a64e4fc --- /dev/null +++ b/pkg/cmd/attestation/api/mock_client.go @@ -0,0 +1,71 @@ +package api + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/sigstore/sigstore-go/pkg/bundle" +) + +type MockClient struct { + OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) + OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) +} + +func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { + return m.OnGetByRepoAndDigest(repo, digest, limit) +} + +func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) { + return m.OnGetByOwnerAndDigest(owner, digest, limit) +} + +func makeTestAttestation() Attestation { + bundleBytes, err := os.ReadFile("../test/data/sigstore-js-2.1.0-bundle.json") + if err != nil { + panic(err) + } + + var b *bundle.ProtobufBundle + err = json.Unmarshal(bundleBytes, &b) + if err != nil { + panic(err) + } + + return Attestation{Bundle: b} +} + +func OnGetByRepoAndDigestSuccess(repo, digest string, limit int) ([]*Attestation, error) { + att1 := makeTestAttestation() + att2 := makeTestAttestation() + return []*Attestation{&att1, &att2}, nil +} + +func OnGetByRepoAndDigestFailure(repo, digest string, limit int) ([]*Attestation, error) { + return nil, fmt.Errorf("failed to fetch by repo and digest") +} + +func OnGetByOwnerAndDigestSuccess(owner, digest string, limit int) ([]*Attestation, error) { + att1 := makeTestAttestation() + att2 := makeTestAttestation() + return []*Attestation{&att1, &att2}, nil +} + +func OnGetByOwnerAndDigestFailure(owner, digest string, limit int) ([]*Attestation, error) { + return nil, fmt.Errorf("failed to fetch by owner and digest") +} + +func NewTestClient() *MockClient { + return &MockClient{ + OnGetByRepoAndDigest: OnGetByRepoAndDigestSuccess, + OnGetByOwnerAndDigest: OnGetByOwnerAndDigestSuccess, + } +} + +func NewFailTestClient() *MockClient { + return &MockClient{ + OnGetByRepoAndDigest: OnGetByRepoAndDigestFailure, + OnGetByOwnerAndDigest: OnGetByOwnerAndDigestFailure, + } +} diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go new file mode 100644 index 000000000..de354b947 --- /dev/null +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -0,0 +1,79 @@ +package artifact + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" +) + +type artifactType int + +const ( + ociArtifactType artifactType = iota + fileArtifactType +) + +// DigestedArtifact abstracts the software artifact being verified +type DigestedArtifact struct { + URL string + digest string + digestAlg string +} + +func normalizeReference(reference string, pathSeparator rune) (normalized string, artifactType artifactType, err error) { + switch { + case strings.HasPrefix(reference, "oci://"): + return reference[6:], ociArtifactType, nil + case strings.HasPrefix(reference, "file://"): + uri, err := url.ParseRequestURI(reference) + if err != nil { + return "", 0, fmt.Errorf("failed to parse reference URI: %v", err) + } + var path string + if pathSeparator == '/' { + // Unix paths use forward slashes like URIs, so no need to modify + path = uri.Path + } else { + // Windows paths should be normalized to use backslashes + path = strings.ReplaceAll(uri.Path, "/", string(pathSeparator)) + // Remove leading slash from Windows paths if present + if strings.HasPrefix(path, string(pathSeparator)) { + path = path[1:] + } + } + return filepath.Clean(path), fileArtifactType, nil + } + // Treat any other reference as a local file path + return filepath.Clean(reference), fileArtifactType, nil +} + +func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) { + normalized, artifactType, err := normalizeReference(reference, os.PathSeparator) + if err != nil { + return nil, err + } + if artifactType == ociArtifactType { + // TODO: should we allow custom digestAlg for OCI artifacts? + return digestContainerImageArtifact(normalized, client) + } + return digestLocalFileArtifact(normalized, digestAlg) +} + +// Digest returns the artifact's digest +func (a *DigestedArtifact) Digest() string { + return a.digest +} + +// Algorithm returns the artifact's algorithm +func (a *DigestedArtifact) Algorithm() string { + return a.digestAlg +} + +// DigestWithAlg returns the digest:algorithm of the artifact +func (a *DigestedArtifact) DigestWithAlg() string { + return fmt.Sprintf("%s:%s", a.digestAlg, a.digest) +} diff --git a/pkg/cmd/attestation/artifact/artifact_posix_test.go b/pkg/cmd/attestation/artifact/artifact_posix_test.go new file mode 100644 index 000000000..31e9cb7e7 --- /dev/null +++ b/pkg/cmd/attestation/artifact/artifact_posix_test.go @@ -0,0 +1,99 @@ +//go:build !windows +// +build !windows + +package artifact + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNormalizeReference(t *testing.T) { + testCases := []struct { + name string + reference string + pathSeparator rune + expectedResult string + expectedType artifactType + expectedError bool + }{ + { + name: "file reference without scheme", + reference: "/path/to/file", + pathSeparator: '/', + expectedResult: "/path/to/file", + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "file scheme uri with %20", + reference: "file:///path/to/file%20with%20spaces", + pathSeparator: '/', + expectedResult: "/path/to/file with spaces", + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "windows file reference without scheme", + reference: `c:\path\to\file`, + pathSeparator: '\\', + expectedResult: `c:\path\to\file`, + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "file reference with scheme", + reference: "file:///path/to/file", + pathSeparator: '/', + expectedResult: "/path/to/file", + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "windows path", + reference: "file:///C:/path/to/file", + pathSeparator: '\\', + expectedResult: `C:\path\to\file`, + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "windows path with backslashes", + reference: "file:///C:\\path\\to\\file", + pathSeparator: '\\', + expectedResult: `C:\path\to\file`, + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "oci reference", + reference: "oci://example.com/repo:tag", + pathSeparator: '/', + expectedResult: "example.com/repo:tag", + expectedType: ociArtifactType, + expectedError: false, + }, + { + name: "oci reference with digest", + reference: "oci://example.com/repo@sha256:abcdef1234567890", + pathSeparator: '/', + expectedResult: "example.com/repo@sha256:abcdef1234567890", + expectedType: ociArtifactType, + expectedError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, artifactType, err := normalizeReference(tc.reference, tc.pathSeparator) + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedResult, result) + require.Equal(t, tc.expectedType, artifactType) + } + }) + } +} diff --git a/pkg/cmd/attestation/artifact/artifact_windows_test.go b/pkg/cmd/attestation/artifact/artifact_windows_test.go new file mode 100644 index 000000000..46995f226 --- /dev/null +++ b/pkg/cmd/attestation/artifact/artifact_windows_test.go @@ -0,0 +1,59 @@ +//go:build windows +// +build windows + +package artifact + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNormalizeReference(t *testing.T) { + testCases := []struct { + name string + reference string + pathSeparator rune + expectedResult string + expectedType artifactType + expectedError bool + }{ + { + name: "windows file reference without scheme", + reference: `c:\path\to\file`, + pathSeparator: '\\', + expectedResult: `c:\path\to\file`, + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "windows path", + reference: "file:///C:/path/to/file", + pathSeparator: '\\', + expectedResult: `C:\path\to\file`, + expectedType: fileArtifactType, + expectedError: false, + }, + { + name: "windows path with backslashes", + reference: "file:///C:\\path\\to\\file", + pathSeparator: '\\', + expectedResult: `C:\path\to\file`, + expectedType: fileArtifactType, + expectedError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, artifactType, err := normalizeReference(tc.reference, tc.pathSeparator) + if tc.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedResult, result) + require.Equal(t, tc.expectedType, artifactType) + } + }) + } +} diff --git a/pkg/cmd/attestation/artifact/digest/digest.go b/pkg/cmd/attestation/artifact/digest/digest.go new file mode 100644 index 000000000..e48fb1d0d --- /dev/null +++ b/pkg/cmd/attestation/artifact/digest/digest.go @@ -0,0 +1,53 @@ +package digest + +import ( + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "fmt" + "hash" + "io" +) + +const ( + SHA256DigestAlgorithm = "sha256" + SHA512DigestAlgorithm = "sha512" +) + +var ( + errUnsupportedAlgorithm = fmt.Errorf("unsupported digest algorithm") + validDigestAlgorithms = [...]string{SHA256DigestAlgorithm, SHA512DigestAlgorithm} +) + +// IsValidDigestAlgorithm returns true if the provided algorithm is supported +func IsValidDigestAlgorithm(alg string) bool { + for _, a := range validDigestAlgorithms { + if a == alg { + return true + } + } + return false +} + +// ValidDigestAlgorithms returns a list of supported digest algorithms +func ValidDigestAlgorithms() []string { + return validDigestAlgorithms[:] +} + +func CalculateDigestWithAlgorithm(r io.Reader, alg string) (string, error) { + var h hash.Hash + switch alg { + case SHA256DigestAlgorithm: + h = sha256.New() + case SHA512DigestAlgorithm: + h = sha512.New() + default: + return "", errUnsupportedAlgorithm + } + + if _, err := io.Copy(h, r); err != nil { + return "", fmt.Errorf("failed to calculate digest: %v", err) + } + digest := h.Sum(nil) + return hex.EncodeToString(digest), nil +} diff --git a/pkg/cmd/attestation/artifact/digest/digest_test.go b/pkg/cmd/attestation/artifact/digest/digest_test.go new file mode 100644 index 000000000..bcfd2c1ac --- /dev/null +++ b/pkg/cmd/attestation/artifact/digest/digest_test.go @@ -0,0 +1,46 @@ +package digest + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestArtifactDigestWithAlgorithm(t *testing.T) { + testString := "deadbeef" + sha512TestDigest := "113a3bc783d851fc0373214b19ea7be9fa3de541ecb9fe026d52c603e8ea19c174cc0e9705f8b90d312212c0c3a6d8453ddfb3e3141409cf4bedc8ef033590b4" + sha256TestDigest := "2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" + + t.Run("sha256", func(t *testing.T) { + reader := strings.NewReader(testString) + digest, err := CalculateDigestWithAlgorithm(reader, "sha256") + assert.Nil(t, err) + assert.Equal(t, sha256TestDigest, digest) + }) + + t.Run("sha512", func(t *testing.T) { + reader := strings.NewReader(testString) + digest, err := CalculateDigestWithAlgorithm(reader, "sha512") + assert.Nil(t, err) + assert.Equal(t, sha512TestDigest, digest) + }) + + t.Run("fail with sha384", func(t *testing.T) { + reader := strings.NewReader(testString) + _, err := CalculateDigestWithAlgorithm(reader, "sha384") + require.Error(t, err) + require.ErrorAs(t, err, &errUnsupportedAlgorithm) + }) +} + +func TestValidDigestAlgorithms(t *testing.T) { + t.Run("includes sha256", func(t *testing.T) { + assert.Contains(t, ValidDigestAlgorithms(), "sha256") + }) + + t.Run("includes sha512", func(t *testing.T) { + assert.Contains(t, ValidDigestAlgorithms(), "sha512") + }) +} diff --git a/pkg/cmd/attestation/artifact/file.go b/pkg/cmd/attestation/artifact/file.go new file mode 100644 index 000000000..789a92a5d --- /dev/null +++ b/pkg/cmd/attestation/artifact/file.go @@ -0,0 +1,25 @@ +package artifact + +import ( + "fmt" + "os" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/digest" +) + +func digestLocalFileArtifact(filename, digestAlg string) (*DigestedArtifact, error) { + data, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("failed to get open local artifact: %v", err) + } + defer data.Close() + digest, err := digest.CalculateDigestWithAlgorithm(data, digestAlg) + if err != nil { + return nil, fmt.Errorf("failed to calculate local artifact digest: %v", err) + } + return &DigestedArtifact{ + URL: fmt.Sprintf("file://%s", filename), + digest: digest, + digestAlg: digestAlg, + }, nil +} diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go new file mode 100644 index 000000000..2af13e723 --- /dev/null +++ b/pkg/cmd/attestation/artifact/image.go @@ -0,0 +1,28 @@ +package artifact + +import ( + "fmt" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/distribution/reference" +) + +func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtifact, error) { + // try to parse the url as a valid registry reference + named, err := reference.Parse(url) + if err != nil { + // cannot be parsed as a registry reference + return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err) + } + + digest, err := client.GetImageDigest(named.String()) + if err != nil { + return nil, err + } + + return &DigestedArtifact{ + URL: fmt.Sprintf("oci://%s", named.String()), + digest: digest.Hex, + digestAlg: digest.Algorithm, + }, nil +} diff --git a/pkg/cmd/attestation/artifact/image_test.go b/pkg/cmd/attestation/artifact/image_test.go new file mode 100644 index 000000000..5ea5f9a37 --- /dev/null +++ b/pkg/cmd/attestation/artifact/image_test.go @@ -0,0 +1,52 @@ +package artifact + +import ( + "fmt" + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/stretchr/testify/require" +) + +func TestDigestContainerImageArtifact(t *testing.T) { + expectedDigest := "1234567890abcdef" + client := oci.MockClient{} + url := "example.com/repo:tag" + digestedArtifact, err := digestContainerImageArtifact(url, client) + require.NoError(t, err) + require.Equal(t, fmt.Sprintf("oci://%s", url), digestedArtifact.URL) + require.Equal(t, expectedDigest, digestedArtifact.digest) + require.Equal(t, "sha256", digestedArtifact.digestAlg) +} + +func TestParseImageRefFailure(t *testing.T) { + client := oci.ReferenceFailClient{} + url := "example.com/repo:tag" + _, err := digestContainerImageArtifact(url, client) + require.Error(t, err) +} + +func TestFetchImageFailure(t *testing.T) { + testcase := []struct { + name string + client oci.Client + expectedErr error + }{ + { + name: "Fail to authorize with registry", + client: oci.AuthFailClient{}, + expectedErr: oci.ErrRegistryAuthz, + }, + { + name: "Fail to fetch image due to denial", + client: oci.DeniedClient{}, + expectedErr: oci.ErrDenied, + }, + } + + for _, tc := range testcase { + url := "example.com/repo:tag" + _, err := digestContainerImageArtifact(url, tc.client) + require.ErrorIs(t, err, tc.expectedErr) + } +} diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go new file mode 100644 index 000000000..5b8d8cf7a --- /dev/null +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -0,0 +1,70 @@ +package oci + +import ( + "errors" + "fmt" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +var ErrDenied = errors.New("the provided token was denied access to the requested resource, please check the token's expiration and repository access") +var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again") + +type Client interface { + GetImageDigest(imgName string) (*v1.Hash, error) +} + +func checkForUnauthorizedOrDeniedErr(err transport.Error) error { + for _, diagnostic := range err.Errors { + switch diagnostic.Code { + case transport.UnauthorizedErrorCode: + return ErrRegistryAuthz + case transport.DeniedErrorCode: + return ErrDenied + } + } + return nil +} + +type LiveClient struct { + parseReference func(string, ...name.Option) (name.Reference, error) + get func(name.Reference, ...remote.Option) (*remote.Descriptor, error) +} + +// where name is formed like ghcr.io/github/my-image-repo +func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, error) { + name, err := c.parseReference(imgName) + if err != nil { + return nil, fmt.Errorf("failed to create image tag: %v", err) + } + + // The user must already be authenticated with the container registry + // The authn.DefaultKeychain argument indicates that Get should checks the + // user's configuration for the registry credentials + desc, err := c.get(name, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + var transportErr *transport.Error + if errors.As(err, &transportErr) { + if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil { + return nil, accessErr + } + } + return nil, fmt.Errorf("failed to fetch remote image: %v", err) + } + + return &desc.Digest, nil +} + +// Unlike other parts of this command set, we cannot pass a custom HTTP client +// to the go-containerregistry library. This means we have limited visibility +// into the HTTP requests being made to container registries. +func NewLiveClient() *LiveClient { + return &LiveClient{ + parseReference: name.ParseReference, + get: remote.Get, + } +} diff --git a/pkg/cmd/attestation/artifact/oci/client_test.go b/pkg/cmd/attestation/artifact/oci/client_test.go new file mode 100644 index 000000000..9aa415c47 --- /dev/null +++ b/pkg/cmd/attestation/artifact/oci/client_test.go @@ -0,0 +1,83 @@ +package oci + +import ( + "fmt" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + + "github.com/stretchr/testify/require" +) + +func TestGetImageDigest_Success(t *testing.T) { + expectedDigest := v1.Hash{ + Hex: "1234567890abcdef", + Algorithm: "sha256", + } + + c := LiveClient{ + parseReference: func(string, ...name.Option) (name.Reference, error) { + return name.Tag{}, nil + }, + get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) { + d := remote.Descriptor{} + d.Digest = expectedDigest + + return &d, nil + }, + } + + digest, err := c.GetImageDigest("test") + require.NoError(t, err) + require.Equal(t, &expectedDigest, digest) +} + +func TestGetImageDigest_ReferenceFail(t *testing.T) { + c := LiveClient{ + parseReference: func(string, ...name.Option) (name.Reference, error) { + return nil, fmt.Errorf("failed to parse reference") + }, + get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) { + return nil, nil + }, + } + + digest, err := c.GetImageDigest("test") + require.Error(t, err) + require.Nil(t, digest) +} + +func TestGetImageDigest_AuthFail(t *testing.T) { + c := LiveClient{ + parseReference: func(string, ...name.Option) (name.Reference, error) { + return name.Tag{}, nil + }, + get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) { + return nil, &transport.Error{Errors: []transport.Diagnostic{{Code: transport.UnauthorizedErrorCode}}} + }, + } + + digest, err := c.GetImageDigest("test") + require.Error(t, err) + require.ErrorIs(t, err, ErrRegistryAuthz) + require.Nil(t, digest) +} + +func TestGetImageDigest_Denied(t *testing.T) { + c := LiveClient{ + parseReference: func(string, ...name.Option) (name.Reference, error) { + return name.Tag{}, nil + }, + get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) { + return nil, &transport.Error{Errors: []transport.Diagnostic{{Code: transport.DeniedErrorCode}}} + }, + } + + digest, err := c.GetImageDigest("test") + require.Error(t, err) + require.ErrorIs(t, err, ErrDenied) + require.Nil(t, digest) +} diff --git a/pkg/cmd/attestation/artifact/oci/mock_client.go b/pkg/cmd/attestation/artifact/oci/mock_client.go new file mode 100644 index 000000000..24368dec8 --- /dev/null +++ b/pkg/cmd/attestation/artifact/oci/mock_client.go @@ -0,0 +1,34 @@ +package oci + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/v1" +) + +type MockClient struct{} + +func (c MockClient) GetImageDigest(imgName string) (*v1.Hash, error) { + return &v1.Hash{ + Hex: "1234567890abcdef", + Algorithm: "sha256", + }, nil +} + +type ReferenceFailClient struct{} + +func (c ReferenceFailClient) GetImageDigest(imgName string) (*v1.Hash, error) { + return nil, fmt.Errorf("failed to parse reference") +} + +type AuthFailClient struct{} + +func (c AuthFailClient) GetImageDigest(imgName string) (*v1.Hash, error) { + return nil, ErrRegistryAuthz +} + +type DeniedClient struct{} + +func (c DeniedClient) GetImageDigest(imgName string) (*v1.Hash, error) { + return nil, ErrDenied +} diff --git a/pkg/cmd/attestation/attestation.go b/pkg/cmd/attestation/attestation.go new file mode 100644 index 000000000..9b3a0cd2c --- /dev/null +++ b/pkg/cmd/attestation/attestation.go @@ -0,0 +1,28 @@ +package attestation + +import ( + "github.com/cli/cli/v2/pkg/cmd/attestation/download" + "github.com/cli/cli/v2/pkg/cmd/attestation/inspect" + "github.com/cli/cli/v2/pkg/cmd/attestation/tufrootverify" + "github.com/cli/cli/v2/pkg/cmd/attestation/verify" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/spf13/cobra" +) + +func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command { + root := &cobra.Command{ + Use: "attestation [subcommand]", + Short: "Work with artifact attestations", + Aliases: []string{"at"}, + Hidden: true, + Long: "Download and verify artifact attestations.", + } + + root.AddCommand(download.NewDownloadCmd(f, nil)) + root.AddCommand(inspect.NewInspectCmd(f, nil)) + root.AddCommand(verify.NewVerifyCmd(f, nil)) + root.AddCommand(tufrootverify.NewTUFRootVerifyCmd(f, nil)) + + return root +} diff --git a/pkg/cmd/attestation/auth/host.go b/pkg/cmd/attestation/auth/host.go new file mode 100644 index 000000000..998dcb7f5 --- /dev/null +++ b/pkg/cmd/attestation/auth/host.go @@ -0,0 +1,17 @@ +package auth + +import ( + "errors" + + "github.com/cli/go-gh/v2/pkg/auth" +) + +var ErrUnsupportedHost = errors.New("The GH_HOST environment variable is set to a custom GitHub host. gh attestation does not currently support custom GitHub Enterprise hosts") + +func IsHostSupported() error { + host, _ := auth.DefaultHost() + if host != "github.com" { + return ErrUnsupportedHost + } + return nil +} diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go new file mode 100644 index 000000000..2f40970fc --- /dev/null +++ b/pkg/cmd/attestation/download/download.go @@ -0,0 +1,148 @@ +package download + +import ( + "errors" + "fmt" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/auth" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { + opts := &Options{} + downloadCmd := &cobra.Command{ + Use: "download [ | oci://] [--owner | --repo]", + Args: cobra.ExactArgs(1), + Short: "Download an artifact's Sigstore bundle(s) for offline use", + Long: heredoc.Docf(` + Download an artifact's attestations, aka Sigstore bundle(s), for offline use. + + The command requires either: + * a file path to an artifact, or + * a container image URI (e.g. %[1]soci://%[1]s) + + (Note that if you provide an OCI URL, you must already be authenticated with + its container registry.) + + In addition, the command requires either: + * the %[1]s--owner%[1]s flag (e.g. --owner github), or + * the %[1]s--repo%[1]s flag (e.g. --repo github/example). + + The %[1]s--owner%[1]s flag value must match the name of the GitHub organization + that the artifact is associated with. + + The %[1]s--repo%[1]s flag value must match the name of the GitHub repository + that the artifact is associated with. + + Any associated Sigstore bundle(s) will be written to a file in the + current directory named after the artifact's digest. For example, if the + digest is "sha256:1234", the file will be named "sha256:1234.jsonl". + `, "`"), + Example: heredoc.Doc(` + # Download Sigstore bundle(s) for a local artifact associated with a GitHub organization + $ gh attestation download example.bin -o github + + # Download Sigstore bundle(s) for a local artifact associated with a GitHub repository + $ gh attestation download example.bin -R github/example + + # Download Sigstore bundle(s) for an OCI image associated with a GitHub organization + $ gh attestation download oci://example.com/foo/bar:latest -o github + `), + // PreRunE is used to validate flags before the command is run + // If an error is returned, its message will be printed to the terminal + // along with information about how use the command + PreRunE: func(cmd *cobra.Command, args []string) error { + // Create a logger for use throughout the download command + opts.Logger = io.NewHandler(f.IOStreams) + + // set the artifact path + opts.ArtifactPath = args[0] + + // check that the provided flags are valid + if err := opts.AreFlagsValid(); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + hc, err := f.HttpClient() + if err != nil { + return err + } + opts.APIClient = api.NewLiveClient(hc, opts.Logger) + + opts.OCIClient = oci.NewLiveClient() + + opts.Store = NewLiveStore("") + + if err := auth.IsHostSupported(); err != nil { + return err + } + + if runF != nil { + return runF(opts) + } + + if err := runDownload(opts); err != nil { + return fmt.Errorf("Failed to download the artifact's bundle(s): %v", err) + } + return nil + }, + } + + downloadCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "a GitHub organization to scope attestation lookup by") + downloadCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") + downloadCmd.MarkFlagsMutuallyExclusive("owner", "repo") + downloadCmd.MarkFlagsOneRequired("owner", "repo") + cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") + downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") + + return downloadCmd +} + +func runDownload(opts *Options) error { + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + if err != nil { + return fmt.Errorf("failed to digest artifact: %v", err) + } + + opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath) + + c := verification.FetchAttestationsConfig{ + APIClient: opts.APIClient, + Digest: artifact.DigestWithAlg(), + Limit: opts.Limit, + Owner: opts.Owner, + Repo: opts.Repo, + } + attestations, err := verification.GetRemoteAttestations(c) + if err != nil { + if errors.Is(err, api.ErrNoAttestations{}) { + fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath) + return nil + } + return fmt.Errorf("failed to fetch attestations: %v", err) + } + + metadataFilePath, err := opts.Store.createMetadataFile(artifact.DigestWithAlg(), attestations) + if err != nil { + return fmt.Errorf("failed to write attestation: %v", err) + } + fmt.Fprintf(opts.Logger.IO.Out, "Wrote attestations to file %s.\nAny previous content has been overwritten\n\n", metadataFilePath) + + fmt.Fprint(opts.Logger.IO.Out, + opts.Logger.ColorScheme.Greenf( + "The trusted metadata is now available at %s\n", metadataFilePath, + ), + ) + + return nil +} diff --git a/pkg/cmd/attestation/download/download_test.go b/pkg/cmd/attestation/download/download_test.go new file mode 100644 index 000000000..0762a29da --- /dev/null +++ b/pkg/cmd/attestation/download/download_test.go @@ -0,0 +1,314 @@ +package download + +import ( + "bytes" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + +func TestNewDownloadCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + HttpClient: func() (*http.Client, error) { + reg := &httpmock.Registry{} + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + }, + } + + store := &LiveStore{ + outputPath: t.TempDir(), + } + + testcases := []struct { + name string + cli string + wants Options + wantsErr bool + }{ + { + name: "Invalid digest-alg flag", + cli: fmt.Sprintf("%s --owner sigstore --digest-alg sha384", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha384", + Owner: "sigstore", + Store: store, + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Missing digest-alg flag", + cli: fmt.Sprintf("%s --owner sigstore", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + Store: store, + Limit: 30, + }, + wantsErr: false, + }, + { + name: "Missing owner and repo flags", + cli: artifactPath, + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + Store: store, + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Has both owner and repo flags", + cli: fmt.Sprintf("%s --owner sigstore --repo sigstore/sigstore-js", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + Store: store, + Repo: "sigstore/sigstore-js", + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Uses default limit flag", + cli: fmt.Sprintf("%s --owner sigstore", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + Store: store, + Limit: 30, + }, + wantsErr: false, + }, + { + name: "Uses custom limit flag", + cli: fmt.Sprintf("%s --owner sigstore --limit 101", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + Store: store, + Limit: 101, + }, + wantsErr: false, + }, + { + name: "Uses invalid limit flag", + cli: fmt.Sprintf("%s --owner sigstore --limit 0", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha256", + Owner: "sigstore", + Store: store, + Limit: 0, + }, + wantsErr: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var opts *Options + cmd := NewDownloadCmd(f, func(o *Options) error { + opts = o + return nil + }) + + argv := strings.Split(tc.cli, " ") + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err := cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm) + assert.Equal(t, tc.wants.Limit, opts.Limit) + assert.Equal(t, tc.wants.Owner, opts.Owner) + assert.Equal(t, tc.wants.Repo, opts.Repo) + assert.NotNil(t, opts.APIClient) + assert.NotNil(t, opts.OCIClient) + assert.NotNil(t, opts.Logger) + assert.NotNil(t, opts.Store) + }) + } +} + +func TestRunDownload(t *testing.T) { + tempDir := t.TempDir() + store := &LiveStore{ + outputPath: tempDir, + } + + baseOpts := Options{ + ArtifactPath: artifactPath, + APIClient: api.NewTestClient(), + OCIClient: oci.MockClient{}, + DigestAlgorithm: "sha512", + Owner: "sigstore", + Store: store, + Limit: 30, + Logger: io.NewTestHandler(), + } + + t.Run("fetch and store attestations successfully with owner", func(t *testing.T) { + err := runDownload(&baseOpts) + require.NoError(t, err) + + artifact, err := artifact.NewDigestedArtifact(baseOpts.OCIClient, baseOpts.ArtifactPath, baseOpts.DigestAlgorithm) + require.NoError(t, err) + + require.FileExists(t, fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg())) + + actualLineCount, err := countLines(fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg())) + require.NoError(t, err) + + expectedLineCount := 2 + require.Equal(t, expectedLineCount, actualLineCount) + }) + + t.Run("fetch and store attestations successfully with repo", func(t *testing.T) { + opts := baseOpts + opts.Owner = "" + opts.Repo = "sigstore/sigstore-js" + + err := runDownload(&opts) + require.NoError(t, err) + + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + require.NoError(t, err) + + require.FileExists(t, fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg())) + + actualLineCount, err := countLines(fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg())) + require.NoError(t, err) + + expectedLineCount := 2 + require.Equal(t, expectedLineCount, actualLineCount) + }) + + t.Run("download OCI image attestations successfully", func(t *testing.T) { + opts := baseOpts + opts.ArtifactPath = "oci://ghcr.io/github/test" + + err := runDownload(&opts) + require.NoError(t, err) + + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + require.NoError(t, err) + + require.FileExists(t, fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg())) + + actualLineCount, err := countLines(fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg())) + require.NoError(t, err) + + expectedLineCount := 2 + require.Equal(t, expectedLineCount, actualLineCount) + }) + + t.Run("cannot find artifact", func(t *testing.T) { + opts := baseOpts + opts.ArtifactPath = "../test/data/not-real.zip" + + err := runDownload(&opts) + require.Error(t, err) + }) + + t.Run("no attestations found", func(t *testing.T) { + opts := baseOpts + opts.APIClient = api.MockClient{ + OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) { + return nil, api.ErrNoAttestations{} + }, + } + + err := runDownload(&opts) + require.NoError(t, err) + + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + require.NoError(t, err) + require.NoFileExists(t, artifact.DigestWithAlg()) + }) + + t.Run("failed to fetch attestations", func(t *testing.T) { + opts := baseOpts + opts.APIClient = api.MockClient{ + OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) { + return nil, fmt.Errorf("failed to fetch attestations") + }, + } + + err := runDownload(&opts) + require.Error(t, err) + }) + + t.Run("cannot download OCI artifact", func(t *testing.T) { + opts := baseOpts + opts.ArtifactPath = "oci://ghcr.io/github/test" + opts.OCIClient = oci.ReferenceFailClient{} + + err := runDownload(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "failed to digest artifact") + }) + + t.Run("with missing API client", func(t *testing.T) { + customOpts := baseOpts + customOpts.APIClient = nil + require.Error(t, runDownload(&customOpts)) + }) + + t.Run("fail to write attestations to metadata file", func(t *testing.T) { + opts := baseOpts + opts.Store = &MockStore{ + OnCreateMetadataFile: OnCreateMetadataFileFailure, + } + + err := runDownload(&opts) + require.Error(t, err) + require.ErrorAs(t, err, &ErrAttestationFileCreation) + }) +} diff --git a/pkg/cmd/attestation/download/metadata.go b/pkg/cmd/attestation/download/metadata.go new file mode 100644 index 000000000..4096be001 --- /dev/null +++ b/pkg/cmd/attestation/download/metadata.go @@ -0,0 +1,70 @@ +package download + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" +) + +var ErrAttestationFileCreation = fmt.Errorf("failed to write attestations to file") + +type MetadataStore interface { + createMetadataFile(artifactDigest string, attestationsResp []*api.Attestation) (string, error) +} + +type LiveStore struct { + outputPath string +} + +func (s *LiveStore) createJSONLinesFilePath(artifact string) string { + path := fmt.Sprintf("%s.jsonl", artifact) + if s.outputPath != "" { + return fmt.Sprintf("%s/%s", s.outputPath, path) + } + return path +} + +func (s *LiveStore) createMetadataFile(artifactDigest string, attestationsResp []*api.Attestation) (string, error) { + metadataFilePath := s.createJSONLinesFilePath(artifactDigest) + + f, err := os.Create(metadataFilePath) + if err != nil { + return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to create file: %v", err)) + } + + for _, resp := range attestationsResp { + bundle := resp.Bundle + attBytes, err := json.Marshal(bundle) + if err != nil { + if err = f.Close(); err != nil { + return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to close file while marshalling JSON: %v", err)) + } + return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to marshall attestation to JSON while writing to file: %v", err)) + } + + withNewline := fmt.Sprintf("%s\n", attBytes) + _, err = f.Write([]byte(withNewline)) + if err != nil { + if err = f.Close(); err != nil { + return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to close file while handling write error: %v", err)) + } + + return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to write attestations: %v", err)) + } + } + + if err = f.Close(); err != nil { + return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to close file after writing attestations: %v", err)) + } + + return metadataFilePath, nil +} + +func NewLiveStore(outputPath string) *LiveStore { + return &LiveStore{ + outputPath: outputPath, + } +} diff --git a/pkg/cmd/attestation/download/metadata_test.go b/pkg/cmd/attestation/download/metadata_test.go new file mode 100644 index 000000000..8ee3b2a45 --- /dev/null +++ b/pkg/cmd/attestation/download/metadata_test.go @@ -0,0 +1,87 @@ +package download + +import ( + "bufio" + "fmt" + "os" + "path" + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + + "github.com/stretchr/testify/require" +) + +type MockStore struct { + OnCreateMetadataFile func(artifactDigest string, attestationsResp []*api.Attestation) (string, error) +} + +func (s *MockStore) createMetadataFile(artifact string, attestationsResp []*api.Attestation) (string, error) { + return s.OnCreateMetadataFile(artifact, attestationsResp) +} + +func OnCreateMetadataFileFailure(artifactDigest string, attestationsResp []*api.Attestation) (string, error) { + return "", fmt.Errorf("failed to create trusted metadata file") +} + +func TestCreateJSONLinesFilePath(t *testing.T) { + tempDir := t.TempDir() + artifact, err := artifact.NewDigestedArtifact(oci.MockClient{}, "../test/data/sigstore-js-2.1.0.tgz", "sha512") + require.NoError(t, err) + + outputFileName := fmt.Sprintf("%s.jsonl", artifact.DigestWithAlg()) + + testCases := []struct { + name string + outputPath string + expected string + }{ + { + name: "with output path", + outputPath: tempDir, + expected: path.Join(tempDir, outputFileName), + }, + { + name: "with nested output path", + outputPath: path.Join(tempDir, "subdir"), + expected: path.Join(tempDir, "subdir", outputFileName), + }, + { + name: "with output path with beginning slash", + outputPath: path.Join("/", tempDir, "subdir"), + expected: path.Join("/", tempDir, "subdir", outputFileName), + }, + { + name: "without output path", + outputPath: "", + expected: outputFileName, + }, + } + + for _, tc := range testCases { + store := LiveStore{ + tc.outputPath, + } + + actualPath := store.createJSONLinesFilePath(artifact.DigestWithAlg()) + require.Equal(t, tc.expected, actualPath) + } +} + +func countLines(path string) (int, error) { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + + counter := 0 + scanner := bufio.NewScanner(f) + for scanner.Scan() { + counter += 1 + } + + return counter, nil +} diff --git a/pkg/cmd/attestation/download/options.go b/pkg/cmd/attestation/download/options.go new file mode 100644 index 000000000..6a64ecc30 --- /dev/null +++ b/pkg/cmd/attestation/download/options.go @@ -0,0 +1,35 @@ +package download + +import ( + "fmt" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" +) + +const ( + minLimit = 1 + maxLimit = 1000 +) + +type Options struct { + APIClient api.Client + ArtifactPath string + DigestAlgorithm string + Logger *io.Handler + Limit int + Store MetadataStore + OCIClient oci.Client + Owner string + Repo string +} + +func (opts *Options) AreFlagsValid() error { + // Check that limit is between 1 and 1000 + if opts.Limit < minLimit || opts.Limit > maxLimit { + return fmt.Errorf("limit %d not allowed, must be between %d and %d", opts.Limit, minLimit, maxLimit) + } + + return nil +} diff --git a/pkg/cmd/attestation/download/options_test.go b/pkg/cmd/attestation/download/options_test.go new file mode 100644 index 000000000..800691d79 --- /dev/null +++ b/pkg/cmd/attestation/download/options_test.go @@ -0,0 +1,34 @@ +package download + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAreFlagsValid(t *testing.T) { + tests := []struct { + name string + limit int + }{ + { + name: "Limit is too low", + limit: 0, + }, + { + name: "Limit is too high", + limit: 1001, + }, + } + for _, tc := range tests { + opts := Options{ + Limit: tc.limit, + } + + err := opts.AreFlagsValid() + require.Error(t, err) + expectedErrMsg := fmt.Sprintf("limit %d not allowed, must be between 1 and 1000", tc.limit) + require.ErrorContains(t, err, expectedErrMsg) + } +} diff --git a/pkg/cmd/attestation/inspect/bundle.go b/pkg/cmd/attestation/inspect/bundle.go new file mode 100644 index 000000000..283b2c14e --- /dev/null +++ b/pkg/cmd/attestation/inspect/bundle.go @@ -0,0 +1,128 @@ +package inspect + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" +) + +type workflow struct { + Repository string `json:"repository"` +} + +type externalParameters struct { + Workflow workflow `json:"workflow"` +} + +type githubInfo struct { + RepositoryID string `json:"repository_id"` + RepositoryOwnerId string `json:"repository_owner_id"` +} + +type internalParameters struct { + GitHub githubInfo `json:"github"` +} + +type buildDefinition struct { + ExternalParameters externalParameters `json:"externalParameters"` + InternalParameters internalParameters `json:"internalParameters"` +} + +type metadata struct { + InvocationID string `json:"invocationId"` +} + +type runDetails struct { + Metadata metadata `json:"metadata"` +} + +// Predicate captures the predicate of a given attestation +type Predicate struct { + BuildDefinition buildDefinition `json:"buildDefinition"` + RunDetails runDetails `json:"runDetails"` +} + +// AttestationDetail captures attestation source details +// that will be returned by the inspect command +type AttestationDetail struct { + OrgName string `json:"orgName"` + OrgID string `json:"orgId"` + RepositoryName string `json:"repositoryName"` + RepositoryID string `json:"repositoryId"` + WorkflowID string `json:"workflowId"` +} + +func getOrgAndRepo(repoURL string) (string, string, error) { + after, found := strings.CutPrefix(repoURL, "https://github.com/") + if !found { + return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL) + } + + parts := strings.Split(after, "/") + return parts[0], parts[1], nil +} + +func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) { + envelope, err := attr.Bundle.Envelope() + if err != nil { + return AttestationDetail{}, fmt.Errorf("failed to get envelope from bundle: %v", err) + } + + statement, err := envelope.EnvelopeContent().Statement() + if err != nil { + return AttestationDetail{}, fmt.Errorf("failed to get statement from envelope: %v", err) + } + + var predicate Predicate + predicateJson, err := json.Marshal(statement.Predicate) + if err != nil { + return AttestationDetail{}, fmt.Errorf("failed to marshal predicate: %v", err) + } + + err = json.Unmarshal(predicateJson, &predicate) + if err != nil { + return AttestationDetail{}, fmt.Errorf("failed to unmarshal predicate: %v", err) + } + + org, repo, err := getOrgAndRepo(predicate.BuildDefinition.ExternalParameters.Workflow.Repository) + if err != nil { + return AttestationDetail{}, fmt.Errorf("failed to parse attestation content: %v", err) + } + + return AttestationDetail{ + OrgName: org, + OrgID: predicate.BuildDefinition.InternalParameters.GitHub.RepositoryOwnerId, + RepositoryName: repo, + RepositoryID: predicate.BuildDefinition.InternalParameters.GitHub.RepositoryID, + WorkflowID: predicate.RunDetails.Metadata.InvocationID, + }, nil +} + +func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][]string, error) { + details := make([][]string, len(results)) + + for i, result := range results { + detail, err := getAttestationDetail(*result.Attestation) + if err != nil { + return nil, fmt.Errorf("failed to get attestation detail: %v", err) + } + details[i] = []string{detail.RepositoryName, detail.RepositoryID, detail.OrgName, detail.OrgID, detail.WorkflowID} + } + return details, nil +} + +func getAttestationDetails(results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) { + details := make([]AttestationDetail, len(results)) + + for i, result := range results { + detail, err := getAttestationDetail(*result.Attestation) + if err != nil { + return nil, fmt.Errorf("failed to get attestation detail: %v", err) + } + details[i] = detail + } + return details, nil +} diff --git a/pkg/cmd/attestation/inspect/bundle_test.go b/pkg/cmd/attestation/inspect/bundle_test.go new file mode 100644 index 000000000..d7e9babd8 --- /dev/null +++ b/pkg/cmd/attestation/inspect/bundle_test.go @@ -0,0 +1,46 @@ +package inspect + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + + "github.com/stretchr/testify/require" +) + +func TestGetOrgAndRepo(t *testing.T) { + t.Run("with valid source URL", func(t *testing.T) { + sourceURL := "https://github.com/github/gh-attestation" + org, repo, err := getOrgAndRepo(sourceURL) + require.Nil(t, err) + require.Equal(t, "github", org) + require.Equal(t, "gh-attestation", repo) + }) + + t.Run("with invalid source URL", func(t *testing.T) { + sourceURL := "hub.com/github/gh-attestation" + org, repo, err := getOrgAndRepo(sourceURL) + require.Error(t, err) + require.Zero(t, org) + require.Zero(t, repo) + }) +} + +func TestGetAttestationDetail(t *testing.T) { + bundlePath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json") + + attestations, err := verification.GetLocalAttestations(bundlePath) + require.Len(t, attestations, 1) + require.NoError(t, err) + + attestation := attestations[0] + detail, err := getAttestationDetail(*attestation) + require.NoError(t, err) + + require.Equal(t, "sigstore", detail.OrgName) + require.Equal(t, "71096353", detail.OrgID) + require.Equal(t, "sigstore-js", detail.RepositoryName) + require.Equal(t, "495574555", detail.RepositoryID) + require.Equal(t, "https://github.com/sigstore/sigstore-js/actions/runs/6014488666/attempts/1", detail.WorkflowID) +} diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go new file mode 100644 index 000000000..1b2105da4 --- /dev/null +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -0,0 +1,162 @@ +package inspect + +import ( + "fmt" + + "github.com/cli/cli/v2/internal/tableprinter" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/auth" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { + opts := &Options{} + inspectCmd := &cobra.Command{ + Use: "inspect [ | oci://] --bundle ", + Args: cobra.ExactArgs(1), + Hidden: true, + Short: "Inspect a sigstore bundle", + Long: heredoc.Docf(` + Inspect a downloaded Sigstore bundle for a given artifact. + + The command requires either: + * a relative path to a local artifact, or + * a container image URI (e.g. %[1]soci://%[1]s) + + Note that if you provide an OCI URI for the artifact you must already + be authenticated with a container registry. + + The command also requires the %[1]s--bundle%[1]s flag, which provides a file + path to a previously downloaded Sigstore bundle. (See also the %[1]sdownload%[1]s + command). + + By default, the command will print information about the bundle in a table format. + If the %[1]s--json-result%[1]s flag is provided, the command will print the + information in JSON format. + `, "`"), + Example: heredoc.Doc(` + # Inspect a Sigstore bundle and print the results in table format + $ gh attestation inspect --bundle + + # Inspect a Sigstore bundle and print the results in JSON format + $ gh attestation inspect --bundle --json-result + + # Inspect a Sigsore bundle for an OCI artifact, and print the results in table format + $ gh attestation inspect oci:// --bundle + `), + PreRunE: func(cmd *cobra.Command, args []string) error { + // Create a logger for use throughout the inspect command + opts.Logger = io.NewHandler(f.IOStreams) + + // set the artifact path + opts.ArtifactPath = args[0] + + // Clean file path options + // opts.Clean() + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts.OCIClient = oci.NewLiveClient() + + if err := auth.IsHostSupported(); err != nil { + return err + } + + if runF != nil { + return runF(opts) + } + + if err := runInspect(opts); err != nil { + return fmt.Errorf("Failed to inspect the artifact and bundle: %w", err) + } + return nil + }, + } + + inspectCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") + inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck + cmdutil.StringEnumFlag(inspectCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") + cmdutil.AddFormatFlags(inspectCmd, &opts.exporter) + + return inspectCmd +} + +func runInspect(opts *Options) error { + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + if err != nil { + return fmt.Errorf("failed to digest artifact: %s", err) + } + + opts.Logger.Printf("Verifying attestations for the artifact found at %s\n\n", artifact.URL) + + attestations, err := verification.GetLocalAttestations(opts.BundlePath) + if err != nil { + return fmt.Errorf("failed to read attestations for subject: %s", artifact.DigestWithAlg()) + } + + config := verification.SigstoreConfig{ + Logger: opts.Logger, + } + + policy, err := buildPolicy(*artifact) + if err != nil { + return fmt.Errorf("failed to build policy: %v", err) + } + + sigstore, err := verification.NewSigstoreVerifier(config, policy) + if err != nil { + return err + } + + res := sigstore.Verify(attestations) + if res.Error != nil { + return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", res.Error) + } + + opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green( + "Successfully verified all attestations against Sigstore!\n\n", + )) + + // If the user provides the --format=json flag, print the results in JSON format + if opts.exporter != nil { + details, err := getAttestationDetails(res.VerifyResults) + if err != nil { + return fmt.Errorf("failed to get attestation detail: %v", err) + } + + // print the results to the terminal as an array of JSON objects + if err = opts.exporter.Write(opts.Logger.IO, details); err != nil { + return fmt.Errorf("failed to write JSON output") + } + return nil + } + + // otherwise, print results in a table + details, err := getDetailsAsSlice(res.VerifyResults) + if err != nil { + return fmt.Errorf("failed to parse attestation details: %v", err) + } + + headers := []string{"Repo Name", "Repo ID", "Org Name", "Org ID", "Workflow ID"} + t := tableprinter.New(opts.Logger.IO, tableprinter.WithHeader(headers...)) + + for _, row := range details { + for _, field := range row { + t.AddField(field, tableprinter.WithTruncate(nil)) + } + t.EndRow() + } + + if err = t.Render(); err != nil { + return fmt.Errorf("failed to print output: %v", err) + } + + return nil +} diff --git a/pkg/cmd/attestation/inspect/inspect_test.go b/pkg/cmd/attestation/inspect/inspect_test.go new file mode 100644 index 000000000..e42bb2620 --- /dev/null +++ b/pkg/cmd/attestation/inspect/inspect_test.go @@ -0,0 +1,179 @@ +package inspect + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main" + SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/" +) + +var ( + artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json") +) + +func TestNewInspectCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + HttpClient: func() (*http.Client, error) { + reg := &httpmock.Registry{} + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + }, + } + + testcases := []struct { + name string + cli string + wants Options + wantsErr bool + wantsExporter bool + }{ + { + name: "Invalid digest-alg flag", + cli: fmt.Sprintf("%s --bundle %s --digest-alg sha384", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha384", + OCIClient: oci.MockClient{}, + }, + wantsErr: true, + }, + { + name: "Use default digest-alg value", + cli: fmt.Sprintf("%s --bundle %s", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha256", + OCIClient: oci.MockClient{}, + }, + wantsErr: false, + }, + { + name: "Use custom digest-alg value", + cli: fmt.Sprintf("%s --bundle %s --digest-alg sha512", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha512", + OCIClient: oci.MockClient{}, + }, + wantsErr: false, + }, + { + name: "Missing bundle flag", + cli: artifactPath, + wants: Options{ + ArtifactPath: artifactPath, + DigestAlgorithm: "sha256", + OCIClient: oci.MockClient{}, + }, + wantsErr: true, + }, + { + name: "Prints output in JSON format", + cli: fmt.Sprintf("%s --bundle %s --format json", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha256", + OCIClient: oci.MockClient{}, + }, + wantsExporter: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var opts *Options + cmd := NewInspectCmd(f, func(o *Options) error { + opts = o + return nil + }) + + argv := strings.Split(tc.cli, " ") + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err := cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath) + assert.Equal(t, tc.wants.BundlePath, opts.BundlePath) + assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm) + assert.NotNil(t, opts.OCIClient) + assert.NotNil(t, opts.Logger) + assert.Equal(t, tc.wantsExporter, opts.exporter != nil) + }) + } +} + +func TestRunInspect(t *testing.T) { + opts := Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha512", + Logger: io.NewTestHandler(), + OCIClient: oci.MockClient{}, + } + + t.Run("with valid artifact and bundle", func(t *testing.T) { + require.Nil(t, runInspect(&opts)) + }) + + t.Run("with missing artifact path", func(t *testing.T) { + customOpts := opts + customOpts.ArtifactPath = test.NormalizeRelativePath("../test/data/non-existent-artifact.zip") + require.Error(t, runInspect(&customOpts)) + }) + + t.Run("with missing bundle path", func(t *testing.T) { + customOpts := opts + customOpts.BundlePath = test.NormalizeRelativePath("../test/data/non-existent-sigstoreBundle.json") + require.Error(t, runInspect(&customOpts)) + }) +} + +func TestJSONOutput(t *testing.T) { + testIO, _, out, _ := iostreams.Test() + opts := Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha512", + Logger: io.NewHandler(testIO), + OCIClient: oci.MockClient{}, + exporter: cmdutil.NewJSONExporter(), + } + require.Nil(t, runInspect(&opts)) + + var target []AttestationDetail + err := json.Unmarshal(out.Bytes(), &target) + require.NoError(t, err) +} diff --git a/pkg/cmd/attestation/inspect/options.go b/pkg/cmd/attestation/inspect/options.go new file mode 100644 index 000000000..56199e06b --- /dev/null +++ b/pkg/cmd/attestation/inspect/options.go @@ -0,0 +1,24 @@ +package inspect + +import ( + "path/filepath" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmdutil" +) + +// Options captures the options for the inspect command +type Options struct { + ArtifactPath string + BundlePath string + DigestAlgorithm string + Logger *io.Handler + OCIClient oci.Client + exporter cmdutil.Exporter +} + +// Clean cleans the file path option values +func (opts *Options) Clean() { + opts.BundlePath = filepath.Clean(opts.BundlePath) +} diff --git a/pkg/cmd/attestation/inspect/policy.go b/pkg/cmd/attestation/inspect/policy.go new file mode 100644 index 000000000..49313d35a --- /dev/null +++ b/pkg/cmd/attestation/inspect/policy.go @@ -0,0 +1,18 @@ +package inspect + +import ( + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + + sigstoreVerify "github.com/sigstore/sigstore-go/pkg/verify" +) + +func buildPolicy(a artifact.DigestedArtifact) (sigstoreVerify.PolicyBuilder, error) { + artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(a) + if err != nil { + return sigstoreVerify.PolicyBuilder{}, err + } + + policy := sigstoreVerify.NewPolicy(artifactDigestPolicyOption, sigstoreVerify.WithoutIdentitiesUnsafe()) + return policy, nil +} diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go new file mode 100644 index 000000000..b41d4ddbf --- /dev/null +++ b/pkg/cmd/attestation/io/handler.go @@ -0,0 +1,61 @@ +package io + +import ( + "fmt" + + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/utils" +) + +type Handler struct { + ColorScheme *iostreams.ColorScheme + IO *iostreams.IOStreams + debugEnabled bool +} + +func NewHandler(io *iostreams.IOStreams) *Handler { + enabled, _ := utils.IsDebugEnabled() + + return &Handler{ + ColorScheme: io.ColorScheme(), + IO: io, + debugEnabled: enabled, + } +} + +func NewTestHandler() *Handler { + testIO, _, _, _ := iostreams.Test() + return NewHandler(testIO) +} + +// Printf writes the formatted arguments to the stderr writer. +func (h *Handler) Printf(f string, v ...interface{}) (int, error) { + if !h.IO.IsStdoutTTY() { + return 0, nil + } + return fmt.Fprintf(h.IO.ErrOut, f, v...) +} + +// Println writes the arguments to the stderr writer with a newline at the end. +func (h *Handler) Println(v ...interface{}) (int, error) { + if !h.IO.IsStdoutTTY() { + return 0, nil + } + return fmt.Fprintln(h.IO.ErrOut, v...) +} + +func (h *Handler) VerbosePrint(msg string) (int, error) { + if !h.debugEnabled || !h.IO.IsStdoutTTY() { + return 0, nil + } + + return fmt.Fprintln(h.IO.ErrOut, msg) +} + +func (h *Handler) VerbosePrintf(f string, v ...interface{}) (int, error) { + if !h.debugEnabled || !h.IO.IsStdoutTTY() { + return 0, nil + } + + return fmt.Fprintf(h.IO.ErrOut, f, v...) +} diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json new file mode 100644 index 000000000..d91176e09 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json @@ -0,0 +1,61 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIIGtDCCBjugAwIBAgIUCJLipSt09KLFc0JYfuDrSan//LswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwODI5MTU0MDIzWhcNMjMwODI5MTU1MDIzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPfm6LPXQeJTC89UOqiNWmnZYGmX4T3iLZGi0EV4bfOoM86Hza94XqyuwxAoWpCPecFCEbAe8l2dg/er3O9LEFqOCBVowggVWMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUeqpCXHr3pcUaL3EFKR+KsmuKQqowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCIGCisGAQQBg78wAQUEFHNpZ3N0b3JlL3NpZ3N0b3JlLWpzMB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwNwYKKwYBBAGDvzABDAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMwOAYKKwYBBAGDvzABDQQqDCgyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0MB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBkGCisGAQQBg78wAQ8ECwwJNDk1NTc0NTU1MCsGCisGAQQBg78wARAEHQwbaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlMBgGCisGAQQBg78wAREECgwINzEwOTYzNTMwZQYKKwYBBAGDvzABEgRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWgYKKwYBBAGDvzABFQRMDEpodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvYWN0aW9ucy9ydW5zLzYwMTQ0ODg2NjYvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABikHz+vwAAAQDAEcwRQIgZEo8c0eCZHEh4uzzJzFz9T+EfSTNTtB2FIH18vXpkOsCIQDE1MTti9RoDnRO3SET1Zkad6FoTx/k6ztQcwIDPmnRxTAKBggqhkjOPQQDAwNnADBkAjBB06fmNXx6ToaClFg2kOxnfLGgrvoR3F5GjDtvDBB8m9SWQNzL211jYmS/g+YbbyUCMC+ad6jIK+efe4XOIlhLcWxeZbBtMjKSrPxmm4jR3BFQQOBR+8r27CioyhxoSqvLuw==" + } + ] + }, + "tlogEntries": [ + { + "logIndex": "33351527", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1693323623", + "inclusionPromise": { + "signedEntryTimestamp": "MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI" + }, + "inclusionProof": { + "logIndex": "29188096", + "rootHash": "fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=", + "treeSize": "29188099", + "hashes": [ + "z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=", + "/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=", + "vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=", + "Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=", + "bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=", + "HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=", + "uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=", + "eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=", + "uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=", + "jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=", + "rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0=" + } + ], + "timestampVerificationData": null + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==", + "keyid": "" + } + ] + } + } diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz new file mode 100644 index 000000000..390b823fd Binary files /dev/null and b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz differ diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl new file mode 100644 index 000000000..a4aca34d0 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl @@ -0,0 +1,2 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.1","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIIGtDCCBjugAwIBAgIUCJLipSt09KLFc0JYfuDrSan//LswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwODI5MTU0MDIzWhcNMjMwODI5MTU1MDIzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPfm6LPXQeJTC89UOqiNWmnZYGmX4T3iLZGi0EV4bfOoM86Hza94XqyuwxAoWpCPecFCEbAe8l2dg/er3O9LEFqOCBVowggVWMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUeqpCXHr3pcUaL3EFKR+KsmuKQqowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCIGCisGAQQBg78wAQUEFHNpZ3N0b3JlL3NpZ3N0b3JlLWpzMB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwNwYKKwYBBAGDvzABDAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMwOAYKKwYBBAGDvzABDQQqDCgyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0MB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBkGCisGAQQBg78wAQ8ECwwJNDk1NTc0NTU1MCsGCisGAQQBg78wARAEHQwbaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlMBgGCisGAQQBg78wAREECgwINzEwOTYzNTMwZQYKKwYBBAGDvzABEgRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWgYKKwYBBAGDvzABFQRMDEpodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvYWN0aW9ucy9ydW5zLzYwMTQ0ODg2NjYvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABikHz+vwAAAQDAEcwRQIgZEo8c0eCZHEh4uzzJzFz9T+EfSTNTtB2FIH18vXpkOsCIQDE1MTti9RoDnRO3SET1Zkad6FoTx/k6ztQcwIDPmnRxTAKBggqhkjOPQQDAwNnADBkAjBB06fmNXx6ToaClFg2kOxnfLGgrvoR3F5GjDtvDBB8m9SWQNzL211jYmS/g+YbbyUCMC+ad6jIK+efe4XOIlhLcWxeZbBtMjKSrPxmm4jR3BFQQOBR+8r27CioyhxoSqvLuw=="}]},"tlogEntries":[{"logIndex":"33351527","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"intoto","version":"0.0.2"},"integratedTime":"1693323623","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI"},"inclusionProof":{"logIndex":"29188096","rootHash":"fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=","treeSize":"29188099","hashes":["z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=","/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=","vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=","Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=","bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=","HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=","uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=","eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=","uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=","jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=","rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM="],"checkpoint":{"envelope":"rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0="}],"timestampVerificationData":null},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==","keyid":""}]}} +{"mediaType":"application/vnd.dev.sigstore.bundle+json;version=0.1","verificationMaterial":{"x509CertificateChain":{"certificates":[{"rawBytes":"MIIGtDCCBjugAwIBAgIUCJLipSt09KLFc0JYfuDrSan//LswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwODI5MTU0MDIzWhcNMjMwODI5MTU1MDIzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPfm6LPXQeJTC89UOqiNWmnZYGmX4T3iLZGi0EV4bfOoM86Hza94XqyuwxAoWpCPecFCEbAe8l2dg/er3O9LEFqOCBVowggVWMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUeqpCXHr3pcUaL3EFKR+KsmuKQqowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCIGCisGAQQBg78wAQUEFHNpZ3N0b3JlL3NpZ3N0b3JlLWpzMB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwNwYKKwYBBAGDvzABDAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMwOAYKKwYBBAGDvzABDQQqDCgyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0MB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBkGCisGAQQBg78wAQ8ECwwJNDk1NTc0NTU1MCsGCisGAQQBg78wARAEHQwbaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlMBgGCisGAQQBg78wAREECgwINzEwOTYzNTMwZQYKKwYBBAGDvzABEgRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWgYKKwYBBAGDvzABFQRMDEpodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvYWN0aW9ucy9ydW5zLzYwMTQ0ODg2NjYvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABikHz+vwAAAQDAEcwRQIgZEo8c0eCZHEh4uzzJzFz9T+EfSTNTtB2FIH18vXpkOsCIQDE1MTti9RoDnRO3SET1Zkad6FoTx/k6ztQcwIDPmnRxTAKBggqhkjOPQQDAwNnADBkAjBB06fmNXx6ToaClFg2kOxnfLGgrvoR3F5GjDtvDBB8m9SWQNzL211jYmS/g+YbbyUCMC+ad6jIK+efe4XOIlhLcWxeZbBtMjKSrPxmm4jR3BFQQOBR+8r27CioyhxoSqvLuw=="}]},"tlogEntries":[{"logIndex":"33351527","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"intoto","version":"0.0.2"},"integratedTime":"1693323623","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI"},"inclusionProof":{"logIndex":"29188096","rootHash":"fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=","treeSize":"29188099","hashes":["z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=","/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=","vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=","Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=","bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=","HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=","uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=","eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=","uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=","jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=","rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM="],"checkpoint":{"envelope":"rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0="}],"timestampVerificationData":null},"dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==","keyid":""}]}} diff --git a/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json new file mode 100644 index 000000000..b1275cebc --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json @@ -0,0 +1,48 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "6751924", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1667948287", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIEzguFRaGzOpMw9JJGUfqSJQ11qlzpcyVCkZfZYPwpLCAiBzdU4LnjtVKYCfyoTImFh3OLFWeOKygtS47Z8fp1GYHg==" + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoidGV4dC9wbGFpbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVU51ZWtORFFXbFhaMEYzU1VKQlowbFZWa2gzWldoUGRFZHVORXRUUkRGSU9GSkpOVGd4VFdaaWVXVjNkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BKZUUxVVFUUk5ha2t4VDBSQk1sZG9ZMDVOYWtsNFRWUkJORTFxVFhkUFJFRXlWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWSFp6WklhbmgwTWxWT2FVb3hhM2QzY1RWWVVVbEpkMDFhYmtwbVZsRXpZa1l3TVhVS1drdDBaVTFrWTFZdk0zRm9RMjFYVDJWamIzaFNjWGR5WWxsVWMyaEhaemxPZVZoalFtSjJaVFo2UzNkYVZsUk1aWEZQUTBGVlVYZG5aMFpCVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZVM1YzQlNDall3YzBOd1oyWjFNRFIzWTNOcWRrTkdlSFF3WmsxcmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQwaDNXVVJXVWpCU1FWRklMMEpDVlhkRk5FVlNXVzVLY0ZsWE5VRmFSMVp2V1ZjeGJHTnBOV3BpTWpCM1RFRlpTMHQzV1VKQ1FVZEVkbnBCUWdwQlVWRmxZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESjRkbG95YkhWTU1qbG9aRmhTYjAxSlIwcENaMjl5UW1kRlJVRmtXalZCWjFGRENrSkljMFZsVVVJelFVaFZRVE5VTUhkaGMySklSVlJLYWtkU05HTnRWMk16UVhGS1MxaHlhbVZRU3pNdmFEUndlV2RET0hBM2J6UkJRVUZIUlZkWVkxSUtPRUZCUVVKQlRVRlNha0pGUVdsQ1VsUnlSMFUxV1RGRmJsbHVhV0ZLUWl0dWMzWTRPVlpoV1hnelVWcHFiMk5GYVc0emNqa3hkMlpyUVVsblRYTnpLd3BtYzNOMU5WTk1VV3R1TjFkRVZFdFlaMjkzTjFONFlraFpVMXBxTTNscmVFRnlWbTUxZWtWM1EyZFpTVXR2V2tsNmFqQkZRWGROUkdGQlFYZGFVVWw0Q2tGUWFsTkhaR1JNU1haNVZVMUhTV3RhSzNVMlNtaEZPWEF4VG1wME0yUkZkSGRaYTAxNFptNUZWakpyTjAxSU1VSldiWGhuT1ZCelNtcHhlV05tYVNzS1pVRkpkMFJoUzI0eVEyUlBlRXR6ZUdObldVNXBORWgyYVVWdVduRjRiV1ZFZVc4eVdVWkpkSHB3U0daSlRWRnRZMUpUYkRreFZXVlBVME00SzFCMVJ3cG5kMDFMQ2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIiwic2lnIjoiVFVWVlEwbERWV2hCVm1WM1puZExiR3MxWmxaNmNGSkVWVkJvUlhjNVR6aEpNbkI0UXpWdVZHNVFabGxFUW5OUFFXbEZRVEJhUm5Gek9UbFJaMUk1YlVGMFJrMVhkRmR5VDJwdFZVTTBOM3BuWVc5dmJFdEpiMHhJTDA5M1pFMDkifV19LCJoYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZGNiNDkyNTljODY2MDdjMzQ2MzVkYWJiNDQzMWYwNjVlOWE3YTczNDcwNGNiNzNmMGFhMGY2YWFhMzg5NmEwNCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjY4ZTY1NmIyNTFlNjdlODM1OGJlZjg0ODNhYjBkNTFjNjYxOWYzZTdhMWE5ZjBlNzU4MzhkNDFmZjM2OGY3MjgifX19fQ==" + } + ], + "timestampVerificationData": { + "rfc3161Timestamps": [] + }, + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIICnzCCAiWgAwIBAgIUVHwehOtGn4KSD1H8RI581MfbyewwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjIxMTA4MjI1ODA2WhcNMjIxMTA4MjMwODA2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGg6Hjxt2UNiJ1kwwq5XQIIwMZnJfVQ3bF01uZKteMdcV/3qhCmWOecoxRqwrbYTshGg9NyXcBbve6zKwZVTLeqOCAUQwggFAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7WpR60sCpgfu04wcsjvCFxt0fMkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEWXcR8AAABAMARjBEAiBRTrGE5Y1EnYniaJB+nsv89VaYx3QZjocEin3r91wfkAIgMss+fssu5SLQkn7WDTKXgow7SxbHYSZj3ykxArVnuzEwCgYIKoZIzj0EAwMDaAAwZQIxAPjSGddLIvyUMGIkZ+u6JhE9p1Njt3dEtwYkMxfnEV2k7MH1BVmxg9PsJjqycfi+eAIwDaKn2CdOxKsxcgYNi4HviEnZqxmeDyo2YFItzpHfIMQmcRSl91UeOSC8+PuGgwMK" + }, + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + } + }, + "dsseEnvelope": { + "payload": "aGVsbG8sIHdvcmxkIQ==", + "payloadType": "text/plain", + "signatures": [ + { + "sig": "NEUCICUhAVewfwKlk5fVzpRDUPhEw9O8I2pxC5nTnPfYDBsOAiEA0ZFqs99QgR9mAtFMWtWrOjmUC47zgaoolKIoLH/OwdM=", + "keyid": "" + } + ] + } +} diff --git a/pkg/cmd/attestation/test/path.go b/pkg/cmd/attestation/test/path.go new file mode 100644 index 000000000..5b6282b7d --- /dev/null +++ b/pkg/cmd/attestation/test/path.go @@ -0,0 +1,13 @@ +package test + +import ( + "runtime" + "strings" +) + +func NormalizeRelativePath(posixPath string) string { + if runtime.GOOS == "windows" { + return strings.ReplaceAll(posixPath, "/", "\\") + } + return posixPath +} diff --git a/pkg/cmd/attestation/tufrootverify/tufrootverify.go b/pkg/cmd/attestation/tufrootverify/tufrootverify.go new file mode 100644 index 000000000..05971da54 --- /dev/null +++ b/pkg/cmd/attestation/tufrootverify/tufrootverify.go @@ -0,0 +1,83 @@ +package tufrootverify + +import ( + "fmt" + "os" + + "github.com/cli/cli/v2/pkg/cmd/attestation/auth" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/MakeNowJust/heredoc" + "github.com/sigstore/sigstore-go/pkg/tuf" + "github.com/spf13/cobra" +) + +func NewTUFRootVerifyCmd(f *cmdutil.Factory, runF func() error) *cobra.Command { + var mirror string + var root string + var cmd = cobra.Command{ + Use: "tuf-root-verify --mirror --root ", + Args: cobra.ExactArgs(0), + Short: "Verify the TUF repository from a provided TUF root", + Hidden: true, + Long: heredoc.Docf(` + Verify a TUF repository with a local TUF root. + + The command requires you provide the %[1]s--mirror%[1]s flag, which should be the URL + of the TUF repository mirror. + + The command also requires you provide the %[1]s--root%[1]s flag, which should be the + path to the TUF root file. + + GitHub relies on TUF to securely deliver the trust root for our signing authority. + For more information on TUF, see the official documentation: . + `, "`"), + Example: heredoc.Doc(` + # Verify the TUF repository from a provided TUF root + gh attestation tuf-root-verify --mirror https://tuf-repo.github.com --root /path/to/1.root.json + `), + RunE: func(cmd *cobra.Command, args []string) error { + if err := auth.IsHostSupported(); err != nil { + return err + } + + if runF != nil { + return runF() + } + + if err := tufRootVerify(mirror, root); err != nil { + return fmt.Errorf("Failed to verify the TUF repository: %w", err) + } + + io := f.IOStreams + fmt.Sprintln(io.Out, io.ColorScheme().Green("Successfully verified the TUF repository")) + return nil + }, + } + + cmd.Flags().StringVarP(&mirror, "mirror", "m", "", "URL to the TUF repository mirror") + cmd.MarkFlagRequired("mirror") //nolint:errcheck + cmd.Flags().StringVarP(&root, "root", "r", "", "Path to the TUF root file on disk") + cmd.MarkFlagRequired("root") //nolint:errcheck + + return &cmd +} + +func tufRootVerify(mirror, root string) error { + rb, err := os.ReadFile(root) + if err != nil { + return fmt.Errorf("failed to read root file %s: %v", root, err) + } + opts := verification.GitHubTUFOptions() + opts.Root = rb + opts.RepositoryBaseURL = mirror + // The purpose is the verify the TUF root and repository, make + // sure there is no caching enabled + opts.CacheValidity = 0 + if _, err = tuf.New(opts); err != nil { + return fmt.Errorf("failed to create TUF client: %v", err) + } + + return nil +} diff --git a/pkg/cmd/attestation/tufrootverify/tufrootverify_test.go b/pkg/cmd/attestation/tufrootverify/tufrootverify_test.go new file mode 100644 index 000000000..004f25618 --- /dev/null +++ b/pkg/cmd/attestation/tufrootverify/tufrootverify_test.go @@ -0,0 +1,80 @@ +package tufrootverify + +import ( + "bytes" + "strings" + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewTUFRootVerifyCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + } + + testcases := []struct { + name string + cli string + wantsErr bool + }{ + { + name: "Missing mirror flag", + cli: "--root ../verification/embed/tuf-repo.github.com/root.json", + wantsErr: true, + }, + { + name: "Missing root flag", + cli: "--mirror https://tuf-repo.github.com", + wantsErr: true, + }, + { + name: "Has all required flags", + cli: "--mirror https://tuf-repo.github.com --root ../verification/embed/tuf-repo.github.com/root.json", + wantsErr: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + cmd := NewTUFRootVerifyCmd(f, func() error { + return nil + }) + + argv := strings.Split(tc.cli, " ") + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err := cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func TestTUFRootVerify(t *testing.T) { + mirror := "https://tuf-repo.github.com" + root := test.NormalizeRelativePath("../verification/embed/tuf-repo.github.com/root.json") + + t.Run("successfully verifies TUF root", func(t *testing.T) { + err := tufRootVerify(mirror, root) + require.NoError(t, err) + }) + + t.Run("fails because the root cannot be found", func(t *testing.T) { + notFoundRoot := test.NormalizeRelativePath("./does/not/exist/root.json") + err := tufRootVerify(mirror, notFoundRoot) + require.Error(t, err) + require.ErrorContains(t, err, "failed to read root file") + }) +} diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go new file mode 100644 index 000000000..7fb4a1615 --- /dev/null +++ b/pkg/cmd/attestation/verification/attestation.go @@ -0,0 +1,115 @@ +package verification + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + "github.com/sigstore/sigstore-go/pkg/bundle" +) + +var ErrLocalAttestations = errors.New("failed to load local attestations") + +type FetchAttestationsConfig struct { + APIClient api.Client + BundlePath string + Digest string + Limit int + Owner string + Repo string +} + +func (c *FetchAttestationsConfig) IsBundleProvided() bool { + return c.BundlePath != "" +} + +func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { + if c.IsBundleProvided() { + return GetLocalAttestations(c.BundlePath) + } + return GetRemoteAttestations(c) +} + +// GetLocalAttestations returns a slice of attestations read from a local bundle file. +func GetLocalAttestations(path string) ([]*api.Attestation, error) { + fileExt := filepath.Ext(path) + switch fileExt { + case ".json": + attestations, err := loadBundleFromJSONFile(path) + if err != nil { + return nil, fmt.Errorf("bundle could not be loaded from JSON file: %v", err) + } + return attestations, nil + case ".jsonl": + attestations, err := loadBundlesFromJSONLinesFile(path) + if err != nil { + return nil, fmt.Errorf("bundles could not be loaded from JSON lines file: %v", err) + } + return attestations, nil + } + return nil, ErrLocalAttestations +} + +func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) { + localAttestation, err := bundle.LoadJSONFromPath(path) + if err != nil { + return nil, err + } + + return []*api.Attestation{{Bundle: localAttestation}}, nil +} + +func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("could not open file: %v", err) + } + defer file.Close() + + attestations := []*api.Attestation{} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + b := scanner.Bytes() + var bundle bundle.ProtobufBundle + bundle.Bundle = new(protobundle.Bundle) + err = bundle.UnmarshalJSON(b) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal bundle from JSON: %v", err) + } + a := api.Attestation{Bundle: &bundle} + attestations = append(attestations, &a) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return attestations, nil +} + +func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { + if c.APIClient == nil { + return nil, fmt.Errorf("api client must be provided") + } + // check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo. + // If Repo is not set, the field will remain empty. It will not be populated using the value of Owner. + if c.Repo != "" { + attestations, err := c.APIClient.GetByRepoAndDigest(c.Repo, c.Digest, c.Limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Repo, err) + } + return attestations, nil + } else if c.Owner != "" { + attestations, err := c.APIClient.GetByOwnerAndDigest(c.Owner, c.Digest, c.Limit) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Owner, err) + } + return attestations, nil + } + return nil, fmt.Errorf("owner or repo must be provided") +} diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go new file mode 100644 index 000000000..b178a0682 --- /dev/null +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -0,0 +1,49 @@ +package verification + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadBundlesFromJSONLinesFile(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + attestations, err := loadBundlesFromJSONLinesFile(path) + + require.NoError(t, err) + require.Len(t, attestations, 2) +} + +func TestLoadBundleFromJSONFile(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0-bundle.json" + attestations, err := loadBundleFromJSONFile(path) + + require.NoError(t, err) + require.Len(t, attestations, 1) +} + +func TestGetLocalAttestations(t *testing.T) { + t.Run("with JSON file containing one bundle", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0-bundle.json" + attestations, err := GetLocalAttestations(path) + + require.NoError(t, err) + require.Len(t, attestations, 1) + }) + + t.Run("with JSON lines file containing multiple bundles", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + attestations, err := GetLocalAttestations(path) + + require.NoError(t, err) + require.Len(t, attestations, 2) + }) + + t.Run("with file with unrecognized extension", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0-bundles.tgz" + attestations, err := GetLocalAttestations(path) + + require.ErrorIs(t, err, ErrLocalAttestations) + require.Nil(t, attestations) + }) +} diff --git a/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json b/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json new file mode 100644 index 000000000..2990a2665 --- /dev/null +++ b/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json @@ -0,0 +1,163 @@ +{ + "signatures": [ + { + "keyid": "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf", + "sig": "304402203c8f5f7443f7052923e82f9ca0b1bb61a33498444076a2f43e1285a47f6e562d022014de99a7e5413440896b6804944e3c49390cfe6e617211b8dc42a8e67675bc13" + }, + { + "keyid": "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "sig": "3044022009a20307f974af7e05cc9564eea497f45062e3b21272d1062713b3d22c868298022059d032ad973a28bdbd03959cf96b21398b6b6e2ca618c17ce6c13712246343a2" + }, + { + "keyid": "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a", + "sig": "3045022100edd270d36d0c8468b9a1f2ef1c81a270c72ffd50c65bca0ed1ebd424df09f64b022002b27ffafd7bc5bdfc25281b5b9b597cf2d67d4eeb4af2ff45eb3e666b860c21" + }, + { + "keyid": "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3", + "sig": "30460221008d7d95434e576d5876b2db30fd645505ca546618bbc7a8e4b39f64e6a36df9ad022100c00a5294e4ddd02d48d28918b87a06bdfdeccd0618ecdcec29bb2597a05fe474" + }, + { + "keyid": "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "sig": "30450220215fb3d19d94560a3a2a6067a71c92daf867d13700c9500c4c32d8009a48a634022100df9fb6cee786313bf6c363daac4de39b3dd531f211f81d2391c41bd2d0f91a80" + }, + { + "keyid": "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228", + "sig": "304502204091ac5e61b6462d262ecc8442781dd09843bed39942a95a4884c8c6a2c212ef022100dcee86392748f48950d04d539ac1a6643ed1f0b4bd6856f8aeb5a135826c846f" + }, + { + "keyid": "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "sig": "30460221009188548601a43b501223caeefca4876ae892e18a85c885004b4c8aeeb05a4421022100abdcc72d94597f8297d6297897ff96f285168dbe6b3d57f846dbc7a2948d2935" + }, + { + "keyid": "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "sig": "3046022100b440561545d48759dc4140cda9f8af7c9405a101d6136dd0a26edd6562b7064f022100cafa917ed90350494e47d226b64a8ec63ef5ceebb8ba4d2dec2ce018e4ad402a" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2024-06-23T08:29:18Z", + "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" + }, + "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC9RNAsuDCNO6T7qA7Y5F8orw2tIW\nr7rUr4ffxvzTMrbkVtjR/trtE0q0+T0zQ8TWLyI6EYMwb947ej2ItfkOyA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@jacobdepriest" + }, + "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", + "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a" + ], + "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", + "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800", + "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c", + "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62", + "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12", + "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a" + ], + "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": 1, + "x-tuf-on-ci-expiry-period": 240, + "x-tuf-on-ci-signing-period": 60 + } +} diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go new file mode 100644 index 000000000..91b54c75e --- /dev/null +++ b/pkg/cmd/attestation/verification/policy.go @@ -0,0 +1,20 @@ +package verification + +import ( + "encoding/hex" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + + "github.com/sigstore/sigstore-go/pkg/verify" +) + +// BuildDigestPolicyOption builds a verify.ArtifactPolicyOption +// from the given artifact digest and digest algorithm +func BuildDigestPolicyOption(a artifact.DigestedArtifact) (verify.ArtifactPolicyOption, error) { + // sigstore-go expects the artifact digest to be decoded from hex + decoded, err := hex.DecodeString(a.Digest()) + if err != nil { + return nil, err + } + return verify.WithArtifactDigest(a.Algorithm(), decoded), nil +} diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go new file mode 100644 index 000000000..daaacf628 --- /dev/null +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -0,0 +1,208 @@ +package verification + +import ( + "fmt" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + + "github.com/sigstore/sigstore-go/pkg/bundle" + "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." +) + +// AttestationProcessingResult captures processing a given attestation's signature verification and policy evaluation +type AttestationProcessingResult struct { + Attestation *api.Attestation `json:"attestation"` + VerificationResult *verify.VerificationResult `json:"verificationResult"` +} + +type SigstoreResults struct { + VerifyResults []*AttestationProcessingResult + Error error +} + +type SigstoreConfig struct { + CustomTrustedRoot string + Logger *io.Handler + NoPublicGood bool +} + +type SigstoreVerifier struct { + ghVerifier *verify.SignedEntityVerifier + publicGoodVerifier *verify.SignedEntityVerifier + customVerifier *verify.SignedEntityVerifier + policy verify.PolicyBuilder + onlyVerifyWithGithub bool + Logger *io.Handler +} + +// NewSigstoreVerifier creates a new SigstoreVerifier struct +// that is used to verify artifacts and attestations against the +// Public Good, GitHub, or a custom trusted root. +func NewSigstoreVerifier(config SigstoreConfig, policy verify.PolicyBuilder) (*SigstoreVerifier, error) { + customVerifier, err := newCustomVerifier(config.CustomTrustedRoot) + if err != nil { + return nil, fmt.Errorf("failed to create custom verifier: %v", err) + } + + publicGoodVerifier, err := newPublicGoodVerifier() + if err != nil { + return nil, fmt.Errorf("failed to create Public Good Sigstore verifier: %v", err) + } + + ghVerifier, err := newGitHubVerifier() + if err != nil { + return nil, fmt.Errorf("failed to create GitHub Sigstore verifier: %v", err) + } + + return &SigstoreVerifier{ + ghVerifier: ghVerifier, + publicGoodVerifier: publicGoodVerifier, + customVerifier: customVerifier, + Logger: config.Logger, + policy: policy, + onlyVerifyWithGithub: config.NoPublicGood, + }, nil +} + +func (v *SigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify.SignedEntityVerifier, string, error) { + verifyContent, err := b.VerificationContent() + if err != nil { + return nil, "", fmt.Errorf("failed to get bundle verification content: %v", err) + } + leafCert, ok := verifyContent.HasCertificate() + if !ok { + return nil, "", fmt.Errorf("leaf cert not found") + } + if len(leafCert.Issuer.Organization) != 1 { + return nil, "", fmt.Errorf("expected the leaf certificate issuer to only have one organization") + } + issuer := leafCert.Issuer.Organization[0] + + // if user provided a custom trusted root file path, use the custom verifier + if v.customVerifier != nil { + return v.customVerifier, issuer, nil + } + + if v.onlyVerifyWithGithub { + return v.ghVerifier, issuer, nil + } + + if leafCert.Issuer.Organization[0] == PublicGoodIssuerOrg { + return v.publicGoodVerifier, issuer, nil + } else if leafCert.Issuer.Organization[0] == GitHubIssuerOrg { + return v.ghVerifier, issuer, nil + } + return nil, "", fmt.Errorf("leaf certificate issuer is not recognized") +} + +func (v *SigstoreVerifier) Verify(attestations []*api.Attestation) *SigstoreResults { + // initialize the processing results before attempting to verify + // with multiple verifiers + results := make([]*AttestationProcessingResult, len(attestations)) + for i, att := range attestations { + apr := &AttestationProcessingResult{ + Attestation: att, + } + results[i] = apr + } + + totalAttestations := len(attestations) + for i, apr := range results { + v.Logger.VerbosePrintf("Verifying attestation %d/%d against the configured Sigstore trust roots\n", i+1, totalAttestations) + + // determine which verifier should attempt verification against the bundle + verifier, issuer, err := v.chooseVerifier(apr.Attestation.Bundle) + if err != nil { + return &SigstoreResults{ + Error: fmt.Errorf("failed to find recognized issuer from bundle content: %v", err), + } + } + + v.Logger.VerbosePrintf("Attempting verification against issuer \"%s\"\n", issuer) + // attempt to verify the attestation + result, err := verifier.Verify(apr.Attestation.Bundle, v.policy) + // if verification fails, create the error and exit verification early + if err != nil { + v.Logger.VerbosePrint(v.Logger.ColorScheme.Redf( + "Failed to verify against issuer \"%s\" \n\n", issuer, + )) + + return &SigstoreResults{ + Error: fmt.Errorf("verifying with issuer \"%s\": %v", issuer, err), + } + } + + // if verification is successful, add the result + // to the AttestationProcessingResult entry + v.Logger.VerbosePrint(v.Logger.ColorScheme.Greenf( + "SUCCESS - attestation signature verified with \"%s\"\n", issuer, + )) + apr.VerificationResult = result + } + + return &SigstoreResults{ + VerifyResults: results, + } +} + +func newCustomVerifier(trustedRootFilePath string) (*verify.SignedEntityVerifier, error) { + if trustedRootFilePath == "" { + return nil, nil + } + + trustedRoot, err := root.NewTrustedRootFromPath(trustedRootFilePath) + if err != nil { + return nil, fmt.Errorf("failed to create trusted root from file %s: %v", trustedRootFilePath, err) + } + + gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1)) + if err != nil { + return nil, fmt.Errorf("failed to create custom verifier: %v", err) + } + + return gv, nil +} + +func newGitHubVerifier() (*verify.SignedEntityVerifier, error) { + opts := GitHubTUFOptions() + client, err := tuf.New(opts) + if err != nil { + return nil, fmt.Errorf("failed to create TUF client: %v", err) + } + trustedRoot, err := root.GetTrustedRoot(client) + if err != nil { + return nil, err + } + gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1)) + if err != nil { + return nil, fmt.Errorf("failed to create GitHub verifier: %v", err) + } + + return gv, nil +} + +func newPublicGoodVerifier() (*verify.SignedEntityVerifier, error) { + client, err := tuf.DefaultClient() + if err != nil { + return nil, fmt.Errorf("failed to create TUF client: %v", err) + } + trustedRoot, err := root.GetTrustedRoot(client) + if err != nil { + return nil, fmt.Errorf("failed to get trusted root: %v", err) + } + + sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1)) + if err != nil { + return nil, fmt.Errorf("failed to create Public Good verifier: %v", err) + } + + return sv, nil +} diff --git a/pkg/cmd/attestation/verification/sigstore_test.go b/pkg/cmd/attestation/verification/sigstore_test.go new file mode 100644 index 000000000..204b5e583 --- /dev/null +++ b/pkg/cmd/attestation/verification/sigstore_test.go @@ -0,0 +1,60 @@ +package verification + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/stretchr/testify/require" +) + +func buildPolicy(a artifact.DigestedArtifact) (verify.PolicyBuilder, error) { + artifactDigestPolicyOption, err := BuildDigestPolicyOption(a) + if err != nil { + return verify.PolicyBuilder{}, err + } + + policy := verify.NewPolicy(artifactDigestPolicyOption, verify.WithoutIdentitiesUnsafe()) + return policy, nil +} + +func TestNewSigstoreVerifier(t *testing.T) { + artifactPath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + artifact, err := artifact.NewDigestedArtifact(nil, artifactPath, "sha512") + require.NoError(t, err) + + policy, err := buildPolicy(*artifact) + require.NoError(t, err) + + c := SigstoreConfig{ + Logger: io.NewTestHandler(), + } + verifier, err := NewSigstoreVerifier(c, policy) + require.NoError(t, err) + + t.Run("with invalid signature", func(t *testing.T) { + bundlePath := test.NormalizeRelativePath("../test/data/sigstoreBundle-invalid-signature.json") + attestations, err := GetLocalAttestations(bundlePath) + require.NotNil(t, attestations) + require.NoError(t, err) + + res := verifier.Verify(attestations) + require.Error(t, res.Error) + require.ErrorContains(t, res.Error, "verifying with issuer \"sigstore.dev\"") + require.Nil(t, res.VerifyResults) + }) + + t.Run("with valid artifact and JSON lines file containing multiple Sigstore bundles", func(t *testing.T) { + bundlePath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + attestations, err := GetLocalAttestations(bundlePath) + require.Len(t, attestations, 2) + require.NoError(t, err) + + res := verifier.Verify(attestations) + require.Len(t, res.VerifyResults, 2) + require.NoError(t, res.Error) + }) +} diff --git a/pkg/cmd/attestation/verification/tuf.go b/pkg/cmd/attestation/verification/tuf.go new file mode 100644 index 000000000..b87f466e8 --- /dev/null +++ b/pkg/cmd/attestation/verification/tuf.go @@ -0,0 +1,41 @@ +package verification + +import ( + _ "embed" + "os" + "path/filepath" + + "github.com/cli/go-gh/v2/pkg/config" + "github.com/sigstore/sigstore-go/pkg/tuf" +) + +//go:embed embed/tuf-repo.github.com/root.json +var githubRoot []byte + +const GitHubTUFMirror = "https://tuf-repo.github.com" + +func DefaultOptionsWithCacheSetting() *tuf.Options { + opts := tuf.DefaultOptions() + + // The CODESPACES environment variable will be set to true in a Codespaces workspace + if os.Getenv("CODESPACES") == "true" { + // if the tool is being used in a Codespace, disable the local cache + // because there is a permissions issue preventing the tuf library + // from writing the Sigstore cache to the home directory + opts.DisableLocalCache = true + } + + // Set the cache path to a directory owned by the CLI + opts.CachePath = filepath.Join(config.CacheDir(), ".sigstore", "root") + + return opts +} + +func GitHubTUFOptions() *tuf.Options { + opts := DefaultOptionsWithCacheSetting() + + opts.Root = githubRoot + opts.RepositoryBaseURL = GitHubTUFMirror + + return opts +} diff --git a/pkg/cmd/attestation/verification/tuf_test.go b/pkg/cmd/attestation/verification/tuf_test.go new file mode 100644 index 000000000..7d816bf82 --- /dev/null +++ b/pkg/cmd/attestation/verification/tuf_test.go @@ -0,0 +1,20 @@ +package verification + +import ( + "os" + "path/filepath" + "testing" + + "github.com/cli/go-gh/v2/pkg/config" + "github.com/stretchr/testify/require" +) + +func TestGitHubTUFOptions(t *testing.T) { + os.Setenv("CODESPACES", "true") + opts := GitHubTUFOptions() + + require.Equal(t, GitHubTUFMirror, opts.RepositoryBaseURL) + require.NotNil(t, opts.Root) + require.True(t, opts.DisableLocalCache) + require.Equal(t, filepath.Join(config.CacheDir(), ".sigstore", "root"), opts.CachePath) +} diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go new file mode 100644 index 000000000..d7742bf3a --- /dev/null +++ b/pkg/cmd/attestation/verify/options.go @@ -0,0 +1,83 @@ +package verify + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmdutil" +) + +// Options captures the options for the verify command +type Options struct { + ArtifactPath string + BundlePath string + CustomTrustedRoot string + DenySelfHostedRunner bool + DigestAlgorithm string + NoPublicGood bool + OIDCIssuer string + Owner string + Repo string + SAN string + SANRegex string + APIClient api.Client + Logger *io.Handler + Limit int + OCIClient oci.Client + exporter cmdutil.Exporter +} + +// Clean cleans the file path option values +func (opts *Options) Clean() { + if opts.BundlePath != "" { + opts.BundlePath = filepath.Clean(opts.BundlePath) + } +} + +func (opts *Options) SetPolicyFlags() { + // check that Repo is in the expected format if provided + if opts.Repo != "" { + // we expect the repo argument to be in the format / + splitRepo := strings.Split(opts.Repo, "/") + + // if Repo is provided but owner is not, set the OWNER portion of the Repo value + // to Owner + opts.Owner = splitRepo[0] + + if opts.SAN == "" && opts.SANRegex == "" { + opts.SANRegex = expandToGitHubURL(opts.Repo) + } + return + } + if opts.SAN == "" && opts.SANRegex == "" { + opts.SANRegex = expandToGitHubURL(opts.Owner) + } +} + +// AreFlagsValid checks that the provided flag combination is valid +// and returns an error otherwise +func (opts *Options) AreFlagsValid() error { + // check that Repo is in the expected format if provided + if opts.Repo != "" { + // we expect the repo argument to be in the format / + splitRepo := strings.Split(opts.Repo, "/") + if len(splitRepo) != 2 { + return fmt.Errorf("invalid value provided for repo: %s", opts.Repo) + } + } + + // Check that limit is between 1 and 1000 + if opts.Limit < 1 || opts.Limit > 1000 { + return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit) + } + + return nil +} + +func expandToGitHubURL(ownerOrRepo string) string { + return fmt.Sprintf("^https://github.com/%s/", ownerOrRepo) +} diff --git a/pkg/cmd/attestation/verify/options_test.go b/pkg/cmd/attestation/verify/options_test.go new file mode 100644 index 000000000..a7430017e --- /dev/null +++ b/pkg/cmd/attestation/verify/options_test.go @@ -0,0 +1,119 @@ +package verify + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + + "github.com/stretchr/testify/require" +) + +var ( + publicGoodArtifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + publicGoodBundlePath = test.NormalizeRelativePath("../test/data/psigstore-js-2.1.0-bundle.json") +) + +func TestAreFlagsValid(t *testing.T) { + t.Run("has invalid Repo value", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + DigestAlgorithm: "sha512", + OIDCIssuer: "some issuer", + Repo: "sigstoresigstore-js", + } + + err := opts.AreFlagsValid() + require.Error(t, err) + require.ErrorContains(t, err, "invalid value provided for repo") + }) + + t.Run("invalid limit < 0", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + BundlePath: publicGoodBundlePath, + DigestAlgorithm: "sha512", + Owner: "sigstore", + OIDCIssuer: "some issuer", + Limit: 0, + } + + err := opts.AreFlagsValid() + require.Error(t, err) + require.ErrorContains(t, err, "limit 0 not allowed, must be between 1 and 1000") + }) + + t.Run("invalid limit > 1000", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + BundlePath: publicGoodBundlePath, + DigestAlgorithm: "sha512", + Owner: "sigstore", + OIDCIssuer: "some issuer", + Limit: 1001, + } + + err := opts.AreFlagsValid() + require.Error(t, err) + require.ErrorContains(t, err, "limit 1001 not allowed, must be between 1 and 1000") + }) +} + +func TestSetPolicyFlags(t *testing.T) { + t.Run("sets Owner and SANRegex when Repo is provided", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + DigestAlgorithm: "sha512", + OIDCIssuer: "some issuer", + Repo: "sigstore/sigstore-js", + } + + opts.SetPolicyFlags() + require.Equal(t, "sigstore", opts.Owner) + require.Equal(t, "sigstore/sigstore-js", opts.Repo) + require.Equal(t, "^https://github.com/sigstore/sigstore-js/", opts.SANRegex) + }) + + t.Run("does not set SANRegex when SANRegex and Repo are provided", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + DigestAlgorithm: "sha512", + OIDCIssuer: "some issuer", + Repo: "sigstore/sigstore-js", + SANRegex: "^https://github/foo", + } + + opts.SetPolicyFlags() + require.Equal(t, "sigstore", opts.Owner) + require.Equal(t, "sigstore/sigstore-js", opts.Repo) + require.Equal(t, "^https://github/foo", opts.SANRegex) + }) + + t.Run("sets SANRegex when Owner is provided", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + BundlePath: publicGoodBundlePath, + DigestAlgorithm: "sha512", + OIDCIssuer: "some issuer", + Owner: "sigstore", + } + + opts.SetPolicyFlags() + require.Equal(t, "sigstore", opts.Owner) + require.Equal(t, "^https://github.com/sigstore/", opts.SANRegex) + }) + + t.Run("does not set SANRegex when SANRegex and Owner are provided", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + BundlePath: publicGoodBundlePath, + DigestAlgorithm: "sha512", + OIDCIssuer: "some issuer", + Owner: "sigstore", + SANRegex: "^https://github/foo", + } + + opts.SetPolicyFlags() + require.Equal(t, "sigstore", opts.Owner) + require.Equal(t, "^https://github/foo", opts.SANRegex) + }) +} diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go new file mode 100644 index 000000000..83d1f62a7 --- /dev/null +++ b/pkg/cmd/attestation/verify/policy.go @@ -0,0 +1,85 @@ +package verify + +import ( + "fmt" + + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/sigstore/sigstore-go/pkg/verify" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" +) + +const ( + GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" + SLSAPredicateType = "https://slsa.dev/provenance/v1" + // represents the GitHub hosted runner in the certificate RunnerEnvironment extension + GitHubRunner = "github-hosted" +) + +func buildSANMatcher(san, sanRegex string) (verify.SubjectAlternativeNameMatcher, error) { + if san == "" && sanRegex == "" { + return verify.SubjectAlternativeNameMatcher{}, nil + } + + sanMatcher, err := verify.NewSANMatcher(san, "", sanRegex) + if err != nil { + return verify.SubjectAlternativeNameMatcher{}, err + } + return sanMatcher, nil +} + +func buildCertificateIdentityOption(opts *Options, runnerEnv string) (verify.PolicyOption, error) { + sanMatcher, err := buildSANMatcher(opts.SAN, opts.SANRegex) + if err != nil { + return nil, err + } + + extensions := certificate.Extensions{ + Issuer: opts.OIDCIssuer, + SourceRepositoryOwnerURI: fmt.Sprintf("https://github.com/%s", opts.Owner), + RunnerEnvironment: runnerEnv, + } + certId, err := verify.NewCertificateIdentity(sanMatcher, extensions) + if err != nil { + return nil, err + } + + return verify.WithCertificateIdentity(certId), nil +} + +func buildVerifyCertIdOption(opts *Options) (verify.PolicyOption, error) { + if opts.DenySelfHostedRunner { + withGHRunner, err := buildCertificateIdentityOption(opts, GitHubRunner) + if err != nil { + return nil, err + } + + return withGHRunner, nil + } + + // if Extensions.RunnerEnvironment value is set to the empty string + // through the second function argument, + // no certificate matching will happen on the RunnerEnvironment field + withAnyRunner, err := buildCertificateIdentityOption(opts, "") + if err != nil { + return nil, err + } + + return withAnyRunner, nil +} + +func buildVerifyPolicy(opts *Options, a artifact.DigestedArtifact) (verify.PolicyBuilder, error) { + artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(a) + if err != nil { + return verify.PolicyBuilder{}, err + } + + certIdOption, err := buildVerifyCertIdOption(opts) + if err != nil { + return verify.PolicyBuilder{}, err + } + + policy := verify.NewPolicy(artifactDigestPolicyOption, certIdOption) + return policy, nil +} diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go new file mode 100644 index 000000000..f15144314 --- /dev/null +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -0,0 +1,31 @@ +package verify + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + + "github.com/stretchr/testify/require" +) + +// This tests that a policy can be built from a valid artifact +// Note that policy use is tested in verify_test.go in this package +func TestBuildPolicy(t *testing.T) { + ociClient := oci.MockClient{} + artifactPath := "../test/data/sigstore-js-2.1.0.tgz" + digestAlg := "sha256" + + artifact, err := artifact.NewDigestedArtifact(ociClient, artifactPath, digestAlg) + require.NoError(t, err) + + opts := &Options{ + ArtifactPath: artifactPath, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + } + + _, err = buildVerifyPolicy(opts, *artifact) + require.NoError(t, err) +} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go new file mode 100644 index 000000000..718f893a2 --- /dev/null +++ b/pkg/cmd/attestation/verify/verify.go @@ -0,0 +1,216 @@ +package verify + +import ( + // "encoding/json" + "errors" + "fmt" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/auth" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +var ErrNoMatchingSLSAPredicate = fmt.Errorf("the attestation does not have the expected SLSA predicate type: %s", SLSAPredicateType) + +func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command { + opts := &Options{} + verifyCmd := &cobra.Command{ + Use: "verify [ | oci://] [--owner | --repo]", + Args: cobra.ExactArgs(1), + Short: "Verify an artifact's integrity using attestations", + Long: heredoc.Docf(` + Verify the integrity and provenance of an artifact using its associated + cryptographically signed attestations. + + The command requires either: + * a file path to an artifact, or + * a container image URI (e.g. %[1]soci://%[1]s) + + (Note that if you provide an OCI URL, you must already be authenticated with + its container registry.) + + In addition, the command requires either: + * the %[1]s--owner%[1]s flag (e.g. --owner github), or + * the %[1]s--repo%[1]s flag (e.g. --repo github/example). + + The %[1]s--owner%[1]s flag value must match the name of the GitHub organization + that the artifact is associated with. + + The %[1]s--repo%[1]s flag value must match the name of the GitHub repository + that the artifact is associated with. + + By default, the verify command will attempt to fetch attestations associated + with the provided artifact from the GitHub API. If you would prefer to verify + the artifact using attestations stored on disk (i.e. from the download command), + provide a path to the %[1]s--bundle%[1]s flag. + + To see the full results that are generated upon successful verification, i.e. + for use with a policy engine, provide the %[1]s--json-result%[1]s flag. + + For more policy verification options, see the other available flags. + `, "`"), + Example: heredoc.Doc(` + # Verify a local artifact associated with a repository + $ gh attestation verify example.bin --repo github/example + + # Verify a local artifact associated with an organization + $ gh attestation verify example.bin --owner github + + # Verify an OCI image using locally stored attestations + $ gh attestation verify oci:// --owner github --bundle sha256:foo.jsonl + `), + // PreRunE is used to validate flags before the command is run + // If an error is returned, its message will be printed to the terminal + // along with information about how use the command + PreRunE: func(cmd *cobra.Command, args []string) error { + // Create a logger for use throughout the verify command + opts.Logger = io.NewHandler(f.IOStreams) + + // set the artifact path + opts.ArtifactPath = args[0] + + // Check that the given flag combination is valid + if err := opts.AreFlagsValid(); err != nil { + return err + } + + // Clean file path options + opts.Clean() + + // set policy flags based on what has been provided + opts.SetPolicyFlags() + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + hc, err := f.HttpClient() + if err != nil { + return err + } + opts.APIClient = api.NewLiveClient(hc, opts.Logger) + + opts.OCIClient = oci.NewLiveClient() + + if err := auth.IsHostSupported(); err != nil { + return err + } + + if runF != nil { + return runF(opts) + } + + if err := runVerify(opts); err != nil { + return fmt.Errorf("Failed to verify the artifact: %v", err) + } + return nil + }, + } + + // general flags + verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") + cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") + verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by") + verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") + verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo") + verifyCmd.MarkFlagsOneRequired("owner", "repo") + verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Only verify attestations signed with GitHub's Sigstore instance") + verifyCmd.Flags().StringVarP(&opts.CustomTrustedRoot, "custom-trusted-root", "", "", "Path to a custom trustedroot.json file to use for verification") + verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") + cmdutil.AddFormatFlags(verifyCmd, &opts.exporter) + // policy enforcement flags + verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners.") + verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly") + verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex") + verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex") + verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token") + + return verifyCmd +} + +func runVerify(opts *Options) error { + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + if err != nil { + return fmt.Errorf("failed to digest artifact: %s", err) + } + + opts.Logger.Printf("Verifying attestations for the artifact found at %s\n", artifact.URL) + + c := verification.FetchAttestationsConfig{ + APIClient: opts.APIClient, + BundlePath: opts.BundlePath, + Digest: artifact.DigestWithAlg(), + Limit: opts.Limit, + Owner: opts.Owner, + Repo: opts.Repo, + } + attestations, err := verification.GetAttestations(c) + if err != nil { + if ok := errors.Is(err, api.ErrNoAttestations{}); ok { + return fmt.Errorf("no attestations found for subject: %s", artifact.DigestWithAlg()) + } + return fmt.Errorf("failed to fetch attestations for subject: %s", artifact.DigestWithAlg()) + } + + policy, err := buildVerifyPolicy(opts, *artifact) + if err != nil { + return fmt.Errorf("failed to build policy: %v", err) + } + + config := verification.SigstoreConfig{ + CustomTrustedRoot: opts.CustomTrustedRoot, + Logger: opts.Logger, + NoPublicGood: opts.NoPublicGood, + } + + sv, err := verification.NewSigstoreVerifier(config, policy) + if err != nil { + return err + } + + sigstoreRes := sv.Verify(attestations) + if sigstoreRes.Error != nil { + return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", sigstoreRes.Error) + } + + opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green( + "Successfully verified all attestations against Sigstore!\n", + )) + + // Try verifying the attestation's predicate type against the expect SLSA predicate type + if err = verifySLSAPredicateType(opts.Logger, sigstoreRes.VerifyResults); err != nil { + return fmt.Errorf("at least one attestation failed to verify predicate type verification: %v", err) + } + + opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green("Successfully verified the SLSA predicate type of all attestations!\n")) + + opts.Logger.Println(opts.Logger.ColorScheme.Green("All attestations have been successfully verified!")) + + if opts.exporter != nil { + // print the results to the terminal as an array of JSON objects + if err = opts.exporter.Write(opts.Logger.IO, sigstoreRes.VerifyResults); err != nil { + return fmt.Errorf("failed to write JSON output") + } + } + + // All attestations passed verification and policy evaluation + return nil +} + +func verifySLSAPredicateType(logger *io.Handler, apr []*verification.AttestationProcessingResult) error { + logger.VerbosePrint("Evaluating attestations have valid SLSA predicate type") + + for _, result := range apr { + if result.VerificationResult.Statement.PredicateType != SLSAPredicateType { + return ErrNoMatchingSLSAPredicate + } + } + + return nil +} diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go new file mode 100644 index 000000000..b4cd864fc --- /dev/null +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -0,0 +1,401 @@ +package verify + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/cli/cli/v2/pkg/cmdutil" + + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/sigstore/sigstore-go/pkg/verify" + + "github.com/stretchr/testify/require" +) + +const ( + SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main" + SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/" +) + +var ( + artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json") +) + +func TestNewVerifyCmd(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: testIO, + HttpClient: func() (*http.Client, error) { + reg := &httpmock.Registry{} + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + }, + } + + testcases := []struct { + name string + cli string + wants Options + wantsErr bool + wantsExporter bool + }{ + { + name: "Invalid digest-alg flag", + cli: fmt.Sprintf("%s --bundle %s --digest-alg sha384 --owner sigstore", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + DigestAlgorithm: "sha384", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + }, + wantsErr: true, + }, + { + name: "Use default digest-alg value", + cli: fmt.Sprintf("%s --bundle %s --owner sigstore", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Use custom digest-alg value", + cli: fmt.Sprintf("%s --bundle %s --owner sigstore --digest-alg sha512", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + DigestAlgorithm: "sha512", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Missing owner and repo flags", + cli: artifactPath, + wants: Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Limit: 30, + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: true, + }, + { + name: "Has both owner and repo flags", + cli: fmt.Sprintf("%s --owner sigstore --repo sigstore/sigstore-js", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Repo: "sigstore/sigstore-js", + Limit: 30, + }, + wantsErr: true, + }, + { + name: "Uses default limit flag", + cli: fmt.Sprintf("%s --owner sigstore", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Uses custom limit flag", + cli: fmt.Sprintf("%s --owner sigstore --limit 101", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Limit: 101, + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: false, + }, + { + name: "Uses invalid limit flag", + cli: fmt.Sprintf("%s --owner sigstore --limit 0", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + DigestAlgorithm: "sha256", + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + Limit: 0, + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: true, + }, + { + name: "Has both cert-identity and cert-identity-regex flags", + cli: fmt.Sprintf("%s --owner sigstore --cert-identity https://github.com/sigstore/ --cert-identity-regex ^https://github.com/sigstore/", artifactPath), + wants: Options{ + ArtifactPath: artifactPath, + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SAN: "https://github.com/sigstore/", + SANRegex: "^https://github.com/sigstore/", + }, + wantsErr: true, + }, + { + name: "Prints output in JSON format", + cli: fmt.Sprintf("%s --bundle %s --owner sigstore --format json", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + }, + wantsExporter: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var opts *Options + cmd := NewVerifyCmd(f, func(o *Options) error { + opts = o + return nil + }) + + argv := strings.Split(tc.cli, " ") + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err := cmd.ExecuteC() + if tc.wantsErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath) + assert.Equal(t, tc.wants.BundlePath, opts.BundlePath) + assert.Equal(t, tc.wants.CustomTrustedRoot, opts.CustomTrustedRoot) + assert.Equal(t, tc.wants.DenySelfHostedRunner, opts.DenySelfHostedRunner) + assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm) + assert.Equal(t, tc.wants.Limit, opts.Limit) + assert.Equal(t, tc.wants.NoPublicGood, opts.NoPublicGood) + assert.Equal(t, tc.wants.OIDCIssuer, opts.OIDCIssuer) + assert.Equal(t, tc.wants.Owner, opts.Owner) + assert.Equal(t, tc.wants.Repo, opts.Repo) + assert.Equal(t, tc.wants.SAN, opts.SAN) + assert.Equal(t, tc.wants.SANRegex, opts.SANRegex) + assert.NotNil(t, opts.APIClient) + assert.NotNil(t, opts.Logger) + assert.NotNil(t, opts.OCIClient) + assert.Equal(t, tc.wantsExporter, opts.exporter != nil) + }) + } +} + +func TestJSONOutput(t *testing.T) { + testIO, _, out, _ := iostreams.Test() + opts := Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha512", + APIClient: api.NewTestClient(), + Logger: io.NewHandler(testIO), + OCIClient: oci.MockClient{}, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + exporter: cmdutil.NewJSONExporter(), + } + require.Nil(t, runVerify(&opts)) + + var target []*verification.AttestationProcessingResult + err := json.Unmarshal(out.Bytes(), &target) + require.NoError(t, err) +} + +func TestRunVerify(t *testing.T) { + logger := io.NewTestHandler() + + publicGoodOpts := Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha512", + APIClient: api.NewTestClient(), + Logger: logger, + OCIClient: oci.MockClient{}, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "^https://github.com/sigstore/", + } + + t.Run("with valid artifact and bundle", func(t *testing.T) { + require.Nil(t, runVerify(&publicGoodOpts)) + }) + + t.Run("with failing OCI artifact fetch", func(t *testing.T) { + opts := publicGoodOpts + opts.ArtifactPath = "oci://ghcr.io/github/test" + opts.OCIClient = oci.ReferenceFailClient{} + + err := runVerify(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "failed to digest artifact") + }) + + t.Run("with missing artifact path", func(t *testing.T) { + opts := publicGoodOpts + opts.ArtifactPath = "../test/data/non-existent-artifact.zip" + require.Error(t, runVerify(&opts)) + }) + + t.Run("with missing bundle path", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "../test/data/non-existent-sigstoreBundle.json" + require.Error(t, runVerify(&opts)) + }) + + t.Run("with owner", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Owner = "sigstore" + + require.Nil(t, runVerify(&opts)) + }) + + t.Run("with repo", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Repo = "github/example" + + require.Nil(t, runVerify(&opts)) + }) + + t.Run("with invalid repo", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Repo = "wrong/example" + opts.APIClient = api.NewFailTestClient() + + err := runVerify(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "failed to fetch attestations for subject") + }) + + t.Run("with invalid owner", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.APIClient = api.NewFailTestClient() + opts.Owner = "wrong-owner" + + err := runVerify(&opts) + require.Error(t, err) + require.ErrorContains(t, err, "failed to fetch attestations for subject") + }) + + t.Run("with invalid OIDC issuer", func(t *testing.T) { + opts := publicGoodOpts + opts.OIDCIssuer = "not-a-real-issuer" + require.Error(t, runVerify(&opts)) + }) + + t.Run("with SAN enforcement", func(t *testing.T) { + opts := Options{ + ArtifactPath: artifactPath, + BundlePath: bundlePath, + APIClient: api.NewTestClient(), + DigestAlgorithm: "sha512", + Logger: logger, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SAN: SigstoreSanValue, + } + require.Nil(t, runVerify(&opts)) + }) + + t.Run("with invalid SAN", func(t *testing.T) { + opts := publicGoodOpts + opts.SAN = "fake san" + require.Error(t, runVerify(&opts)) + }) + + t.Run("with SAN regex enforcement", func(t *testing.T) { + opts := publicGoodOpts + opts.SANRegex = SigstoreSanRegex + require.Nil(t, runVerify(&opts)) + }) + + t.Run("with invalid SAN regex", func(t *testing.T) { + opts := publicGoodOpts + opts.SANRegex = "^https://github.com/sigstore/not-real/" + require.Error(t, runVerify(&opts)) + }) + + t.Run("with no matching OIDC issuer", func(t *testing.T) { + opts := publicGoodOpts + opts.OIDCIssuer = "some-other-issuer" + + require.Error(t, runVerify(&opts)) + }) + + t.Run("with missing API client", func(t *testing.T) { + customOpts := publicGoodOpts + customOpts.APIClient = nil + customOpts.BundlePath = "" + require.Error(t, runVerify(&customOpts)) + }) +} + +func TestVerifySLSAPredicateType_InvalidPredicate(t *testing.T) { + statement := &in_toto.Statement{} + statement.PredicateType = "some-other-predicate-type" + + apr := []*verification.AttestationProcessingResult{ + { + VerificationResult: &verify.VerificationResult{ + Statement: statement, + }, + }, + } + + err := verifySLSAPredicateType(io.NewTestHandler(), apr) + require.Error(t, err) + require.ErrorIs(t, err, ErrNoMatchingSLSAPredicate) +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index db591c27b..4b744e192 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -10,6 +10,7 @@ import ( aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias" "github.com/cli/cli/v2/pkg/cmd/alias/shared" apiCmd "github.com/cli/cli/v2/pkg/cmd/api" + attestationCmd "github.com/cli/cli/v2/pkg/cmd/attestation" authCmd "github.com/cli/cli/v2/pkg/cmd/auth" browseCmd "github.com/cli/cli/v2/pkg/cmd/browse" cacheCmd "github.com/cli/cli/v2/pkg/cmd/cache" @@ -124,6 +125,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, cmd.AddCommand(actionsCmd.NewCmdActions(f)) cmd.AddCommand(aliasCmd.NewCmdAlias(f)) cmd.AddCommand(authCmd.NewCmdAuth(f)) + cmd.AddCommand(attestationCmd.NewCmdAttestation(f)) cmd.AddCommand(configCmd.NewCmdConfig(f)) cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil)) cmd.AddCommand(gistCmd.NewCmdGist(f)) diff --git a/test/integration/attestation-cmd/download-and-verify-package-attestation.sh b/test/integration/attestation-cmd/download-and-verify-package-attestation.sh new file mode 100755 index 000000000..6774b8e2d --- /dev/null +++ b/test/integration/attestation-cmd/download-and-verify-package-attestation.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Get the root directory of the repository +rootDir="$(git rev-parse --show-toplevel)" + +ghBuildPath="$rootDir/bin/gh" + +# Compute the package and attestation URLs +labRatPackageName="sigstore" +latestPackageVersion=$(npm -s info $labRatPackageName dist-tags.latest | tr -d '\n') +packageFile="$labRatPackageName-$latestPackageVersion.tgz" +packageURL="https://registry.npmjs.org/$labRatPackageName/-/$packageFile" +attestationFile="$labRatPackageName-$latestPackageVersion.json" +attestationURL="https://registry.npmjs.org/-/npm/v1/attestations/$labRatPackageName@$latestPackageVersion" + +echo "Testing with package $packageFile and attestation $attestationFile" + +curl -s "$packageURL" -o "$packageFile" +curl -s "$attestationURL" | jq '.attestations[1].bundle' > "$attestationFile" + +# Verify the package with the --owner flag +if ! $ghBuildPath attestation verify "$packageFile" -b "$attestationFile" --digest-alg=sha512 --owner=sigstore; then + # cleanup test data + echo "Failed to verify package with --owner flag" + rm "$packageFile" "$attestationFile" + exit 1 +fi + +if ! $ghBuildPath attestation verify "$packageFile" -b "$attestationFile" --digest-alg=sha512 --repo=sigstore/sigstore-js; then + # cleanup test data + echo "Failed to verify package with --repo flag" + rm "$packageFile" "$attestationFile" + exit 1 +fi + +# cleanup test data +rm "$packageFile" "$attestationFile"