diff --git a/pkg/chains/storage/oci/attestation.go b/pkg/chains/storage/oci/attestation.go index 5856c6d0fe..2d2099058d 100644 --- a/pkg/chains/storage/oci/attestation.go +++ b/pkg/chains/storage/oci/attestation.go @@ -61,7 +61,10 @@ func (s *AttestationStorer) Store(ctx context.Context, req *api.StoreRequest[nam repo = *s.repo } se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...)) - if err != nil { + var entityNotFoundError *ociremote.EntityNotFoundError + if errors.As(err, &entityNotFoundError) { + se = ociremote.SignedUnknown(req.Artifact) + } else if err != nil { return nil, errors.Wrap(err, "getting signed image") } diff --git a/pkg/chains/storage/oci/attestation_test.go b/pkg/chains/storage/oci/attestation_test.go new file mode 100644 index 0000000000..659b787ecd --- /dev/null +++ b/pkg/chains/storage/oci/attestation_test.go @@ -0,0 +1,111 @@ +// Copyright 2025 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oci + +import ( + "fmt" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + intoto "github.com/in-toto/attestation/go/v1" + "github.com/tektoncd/chains/pkg/chains/signing" + "github.com/tektoncd/chains/pkg/chains/storage/api" + logtesting "knative.dev/pkg/logging/testing" +) + +func TestAttestationStorer_Store(t *testing.T) { + tests := []struct { + name string + writeToRegistry func(*testing.T, string) name.Digest + wantErr error + }{ + { + name: "image manifest", + writeToRegistry: func(t *testing.T, registryName string) name.Digest { + t.Helper() + img, err := random.Image(1024, 2) + if err != nil { + t.Fatalf("failed to create random image: %s", err) + } + imgDigest, err := img.Digest() + if err != nil { + t.Fatalf("failed to get image digest: %v", err) + } + ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, imgDigest)) + if err != nil { + t.Fatalf("failed to parse digest: %v", err) + } + if err := remote.Write(ref, img); err != nil { + t.Fatalf("failed to write image to mock registry: %v", err) + } + return ref + }, + }, + { + name: "image layer", + writeToRegistry: func(t *testing.T, registryName string) name.Digest { + t.Helper() + layer, err := random.Layer(1024, types.OCILayer) + if err != nil { + t.Fatalf("failed to create random layer: %v", err) + } + layerDigest, err := layer.Digest() + if err != nil { + t.Fatalf("failed to get layer digest: %v", err) + } + ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, layerDigest)) + if err != nil { + t.Fatalf("failed to parse digest: %v", err) + } + if err := remote.WriteLayer(ref.Repository, layer); err != nil { + t.Fatalf("failed to write layer to mock registry: %v", err) + } + return ref + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := httptest.NewServer(registry.New()) + defer s.Close() + registryName := strings.TrimPrefix(s.URL, "http://") + + ref := tt.writeToRegistry(t, registryName) + + storer, err := NewAttestationStorer(WithTargetRepository(ref.Repository)) + if err != nil { + t.Fatalf("failed to create storer: %v", err) + } + + ctx := logtesting.TestContextWithLogger(t) + _, err = storer.Store(ctx, &api.StoreRequest[name.Digest, *intoto.Statement]{ + Artifact: ref, + Payload: &intoto.Statement{}, + Bundle: &signing.Bundle{}, + }) + + if err != nil { + t.Fatalf("error during Store(): %s", err) + } + }) + } +} diff --git a/pkg/chains/storage/oci/simple.go b/pkg/chains/storage/oci/simple.go index 8cb3c8668f..30952ac7dc 100644 --- a/pkg/chains/storage/oci/simple.go +++ b/pkg/chains/storage/oci/simple.go @@ -57,7 +57,10 @@ func (s *SimpleStorer) Store(ctx context.Context, req *api.StoreRequest[name.Dig logger.Info("Uploading signature") se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...)) - if err != nil { + var entityNotFoundError *ociremote.EntityNotFoundError + if errors.As(err, &entityNotFoundError) { + se = ociremote.SignedUnknown(req.Artifact) + } else if err != nil { return nil, errors.Wrap(err, "getting signed image") } diff --git a/pkg/chains/storage/oci/simple_test.go b/pkg/chains/storage/oci/simple_test.go new file mode 100644 index 0000000000..5e6b3e4479 --- /dev/null +++ b/pkg/chains/storage/oci/simple_test.go @@ -0,0 +1,110 @@ +// Copyright 2025 The Tekton Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oci + +import ( + "fmt" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/tektoncd/chains/pkg/chains/formats/simple" + "github.com/tektoncd/chains/pkg/chains/signing" + "github.com/tektoncd/chains/pkg/chains/storage/api" + logtesting "knative.dev/pkg/logging/testing" +) + +func TestSimpleStorer_Store(t *testing.T) { + tests := []struct { + name string + writeToRegistry func(*testing.T, string) name.Digest + }{ + { + name: "image manifest", + writeToRegistry: func(t *testing.T, registryName string) name.Digest { + t.Helper() + img, err := random.Image(1024, 2) + if err != nil { + t.Fatalf("failed to create random image: %s", err) + } + imgDigest, err := img.Digest() + if err != nil { + t.Fatalf("failed to get image digest: %v", err) + } + ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, imgDigest)) + if err != nil { + t.Fatalf("failed to parse digest: %v", err) + } + if err := remote.Write(ref, img); err != nil { + t.Fatalf("failed to write image to mock registry: %v", err) + } + return ref + }, + }, + { + name: "image layer", + writeToRegistry: func(t *testing.T, registryName string) name.Digest { + t.Helper() + layer, err := random.Layer(1024, types.OCILayer) + if err != nil { + t.Fatalf("failed to create random layer: %s", err) + } + layerDigest, err := layer.Digest() + if err != nil { + t.Fatalf("failed to get layer digest: %v", err) + } + ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, layerDigest)) + if err != nil { + t.Fatalf("failed to parse digest: %v", err) + } + if err := remote.WriteLayer(ref.Repository, layer); err != nil { + t.Fatalf("failed to write layer to mock registry: %v", err) + } + return ref + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := httptest.NewServer(registry.New()) + defer s.Close() + registryName := strings.TrimPrefix(s.URL, "http://") + + ref := tt.writeToRegistry(t, registryName) + + storer, err := NewSimpleStorerFromConfig(WithTargetRepository(ref.Repository)) + if err != nil { + t.Fatalf("failed to create storer: %v", err) + } + + ctx := logtesting.TestContextWithLogger(t) + _, err = storer.Store(ctx, &api.StoreRequest[name.Digest, simple.SimpleContainerImage]{ + Artifact: ref, + Payload: simple.NewSimpleStruct(ref), + Bundle: &signing.Bundle{}, + }) + + if err != nil { + t.Fatalf("error during Store(): %s", err) + } + }) + } +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go new file mode 100644 index 0000000000..d3712767d2 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package random provides a facility for synthesizing pseudo-random images. +package random diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go new file mode 100644 index 0000000000..6121aa715c --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go @@ -0,0 +1,116 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import ( + "archive/tar" + "bytes" + "crypto" + "encoding/hex" + "fmt" + "io" + "math/rand" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// uncompressedLayer implements partial.UncompressedLayer from raw bytes. +type uncompressedLayer struct { + diffID v1.Hash + mediaType types.MediaType + content []byte +} + +// DiffID implements partial.UncompressedLayer +func (ul *uncompressedLayer) DiffID() (v1.Hash, error) { + return ul.diffID, nil +} + +// Uncompressed implements partial.UncompressedLayer +func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(ul.content)), nil +} + +// MediaType returns the media type of the layer +func (ul *uncompressedLayer) MediaType() (types.MediaType, error) { + return ul.mediaType, nil +} + +var _ partial.UncompressedLayer = (*uncompressedLayer)(nil) + +// Image returns a pseudo-randomly generated Image. +func Image(byteSize, layers int64, options ...Option) (v1.Image, error) { + adds := make([]mutate.Addendum, 0, 5) + for i := int64(0); i < layers; i++ { + layer, err := Layer(byteSize, types.DockerLayer, options...) + if err != nil { + return nil, err + } + adds = append(adds, mutate.Addendum{ + Layer: layer, + History: v1.History{ + Author: "random.Image", + Comment: fmt.Sprintf("this is a random history %d of %d", i, layers), + CreatedBy: "random", + }, + }) + } + + return mutate.Append(empty.Image, adds...) +} + +// Layer returns a layer with pseudo-randomly generated content. +func Layer(byteSize int64, mt types.MediaType, options ...Option) (v1.Layer, error) { + o := getOptions(options) + rng := rand.New(o.source) //nolint:gosec + + fileName := fmt.Sprintf("random_file_%d.txt", rng.Int()) + + // Hash the contents as we write it out to the buffer. + var b bytes.Buffer + hasher := crypto.SHA256.New() + mw := io.MultiWriter(&b, hasher) + + // Write a single file with a random name and random contents. + tw := tar.NewWriter(mw) + if err := tw.WriteHeader(&tar.Header{ + Name: fileName, + Size: byteSize, + Typeflag: tar.TypeReg, + }); err != nil { + return nil, err + } + if _, err := io.CopyN(tw, rng, byteSize); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + + h := v1.Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), + } + + return partial.UncompressedToLayer(&uncompressedLayer{ + diffID: h, + mediaType: mt, + content: b.Bytes(), + }) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/random/index.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/index.go new file mode 100644 index 0000000000..4368bddff3 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/index.go @@ -0,0 +1,111 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import ( + "bytes" + "encoding/json" + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type randomIndex struct { + images map[v1.Hash]v1.Image + manifest *v1.IndexManifest +} + +// Index returns a pseudo-randomly generated ImageIndex with count images, each +// having the given number of layers of size byteSize. +func Index(byteSize, layers, count int64, options ...Option) (v1.ImageIndex, error) { + manifest := v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + Manifests: []v1.Descriptor{}, + } + + images := make(map[v1.Hash]v1.Image) + for i := int64(0); i < count; i++ { + img, err := Image(byteSize, layers, options...) + if err != nil { + return nil, err + } + + rawManifest, err := img.RawManifest() + if err != nil { + return nil, err + } + digest, size, err := v1.SHA256(bytes.NewReader(rawManifest)) + if err != nil { + return nil, err + } + mediaType, err := img.MediaType() + if err != nil { + return nil, err + } + + manifest.Manifests = append(manifest.Manifests, v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + }) + + images[digest] = img + } + + return &randomIndex{ + images: images, + manifest: &manifest, + }, nil +} + +func (i *randomIndex) MediaType() (types.MediaType, error) { + return i.manifest.MediaType, nil +} + +func (i *randomIndex) Digest() (v1.Hash, error) { + return partial.Digest(i) +} + +func (i *randomIndex) Size() (int64, error) { + return partial.Size(i) +} + +func (i *randomIndex) IndexManifest() (*v1.IndexManifest, error) { + return i.manifest, nil +} + +func (i *randomIndex) RawManifest() ([]byte, error) { + m, err := i.IndexManifest() + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +func (i *randomIndex) Image(h v1.Hash) (v1.Image, error) { + if img, ok := i.images[h]; ok { + return img, nil + } + + return nil, fmt.Errorf("image not found: %v", h) +} + +func (i *randomIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + // This is a single level index (for now?). + return nil, fmt.Errorf("image not found: %v", h) +} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/random/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/random/options.go new file mode 100644 index 0000000000..af1d2f9695 --- /dev/null +++ b/vendor/github.com/google/go-containerregistry/pkg/v1/random/options.go @@ -0,0 +1,60 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import "math/rand" + +// Option is an optional parameter to the random functions +type Option func(opts *options) + +type options struct { + source rand.Source + + // TODO opens the door to add this in the future + // algorithm digest.Algorithm +} + +func getOptions(opts []Option) *options { + // get a random seed + + // TODO in go 1.20 this is fine (it will be random) + seed := rand.Int63() //nolint:gosec + /* + // in prior go versions this needs to come from crypto/rand + var b [8]byte + _, err := crypto_rand.Read(b[:]) + if err != nil { + panic("cryptographically secure random number generator is not working") + } + seed := int64(binary.LittleEndian.Int64(b[:])) + */ + + // defaults + o := &options{ + source: rand.NewSource(seed), + } + + for _, opt := range opts { + opt(o) + } + return o +} + +// WithSource sets the random number generator source +func WithSource(source rand.Source) Option { + return func(opts *options) { + opts.source = source + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ad50f53486..5b4f5296ab 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1197,6 +1197,7 @@ github.com/google/go-containerregistry/pkg/v1/layout github.com/google/go-containerregistry/pkg/v1/match github.com/google/go-containerregistry/pkg/v1/mutate github.com/google/go-containerregistry/pkg/v1/partial +github.com/google/go-containerregistry/pkg/v1/random github.com/google/go-containerregistry/pkg/v1/remote github.com/google/go-containerregistry/pkg/v1/remote/transport github.com/google/go-containerregistry/pkg/v1/static