diff --git a/go.mod b/go.mod index 77be0c19d..9a2e53d38 100644 --- a/go.mod +++ b/go.mod @@ -92,6 +92,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/certificate-transparency-go v1.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.0 // indirect diff --git a/go.sum b/go.sum index fe7adc3e7..cc92ed459 100644 --- a/go.sum +++ b/go.sum @@ -198,6 +198,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME= github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go index ea055b293..d5c444ee2 100644 --- a/pkg/cmd/attestation/api/attestation.go +++ b/pkg/cmd/attestation/api/attestation.go @@ -26,6 +26,7 @@ func newErrNoAttestations(name, digest string) ErrNoAttestations { type Attestation struct { Bundle *bundle.Bundle `json:"bundle"` + SASUrl string `json:"signedAccessSignatureUrl"` } type AttestationsResponse struct { diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index f47b5f759..f2f9f48f8 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -1,16 +1,21 @@ package api import ( + "encoding/json" "errors" "fmt" "io" "net/http" + "net/url" "strings" "time" "github.com/cenkalti/backoff/v4" "github.com/cli/cli/v2/api" ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/golang/snappy" + v1 "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + "github.com/sigstore/sigstore-go/pkg/bundle" ) const ( @@ -25,6 +30,7 @@ type apiClient interface { } type Client interface { + FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) GetTrustDomain() (string, error) @@ -130,6 +136,49 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At return attestations, nil } +func (c *LiveClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { + fetched := make([]*Attestation, len(attestations)) + for i, a := range attestations { + b, err := c.fetchAttestationWithSASURL(a) + if err != nil { + return nil, fmt.Errorf("failed to fetch attestation with SAS URL: %w", err) + } + fetched[i] = &Attestation{ + Bundle: b, + } + } + return fetched, nil +} + +func (c *LiveClient) fetchAttestationWithSASURL(a *Attestation) (*bundle.Bundle, error) { + if a.SASUrl == "" { + return a.Bundle, nil + } + + parsed, err := url.Parse(a.SASUrl) + if err != nil { + return nil, err + } + + var resp []byte + if err = c.api.REST(parsed.Host, http.MethodGet, parsed.Path, nil, &resp); err != nil { + return nil, err + } + + var decompressedBytes []byte + _, err = snappy.Decode(decompressedBytes, resp) + if err != nil { + return nil, err + } + + var pbBundle *v1.Bundle + if err = json.Unmarshal(decompressedBytes, pbBundle); err != nil { + return nil, err + } + + return bundle.NewBundle(pbBundle) +} + func shouldRetry(err error) bool { var httpError api.HTTPError if errors.As(err, &httpError) { diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go index bcb51c414..17bfdadec 100644 --- a/pkg/cmd/attestation/api/mock_client.go +++ b/pkg/cmd/attestation/api/mock_client.go @@ -12,6 +12,10 @@ type MockClient struct { OnGetTrustDomain func() (string, error) } +func (m MockClient) FetchAttestationsWithSASURL(attestations []*Attestation) ([]*Attestation, error) { + return nil, nil +} + func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { return m.OnGetByRepoAndDigest(repo, digest, limit) } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 0ea91c2f7..a4548a72d 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -127,13 +127,25 @@ func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error if err != nil { return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Repo, err) } - return attestations, nil + + fetched, err := c.APIClient.FetchAttestationsWithSASURL(attestations) + if err != nil { + return nil, fmt.Errorf("failed to fetch bundles from SAS URL: %w", err) + } + + return fetched, 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 + + fetched, err := c.APIClient.FetchAttestationsWithSASURL(attestations) + if err != nil { + return nil, fmt.Errorf("failed to fetch bundles from SAS URL: %w", err) + } + + return fetched, nil } return nil, fmt.Errorf("owner or repo must be provided") }