Skip to content

Commit b15d733

Browse files
authored
Handle signing and attesting blobs (#1395)
Chains fetches existing information about the container images it signs and attests. This works well when the image reference refers to an Image Index or an Image Manifest since those are served from the same endpoint in a container registry. However, when fetching information about a blob (layer) a different endpoint is needed. Cosign handles this behavior by making this step optional: https://github.com/sigstore/cosign/blob/c86498055d0c4ea2f39076064aa094db12f85f6a/cmd/cosign/cli/sign/sign.go#L181-L186 Thus it is possible to sign/attest a blob with the cosign CLI. This commit implements the same logic to Chains so it can also sign/attest blobs. Signed-off-by: Luiz Carvalho <[email protected]>
1 parent ca808c8 commit b15d733

File tree

9 files changed

+533
-2
lines changed

9 files changed

+533
-2
lines changed

pkg/chains/storage/oci/attestation.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ func (s *AttestationStorer) Store(ctx context.Context, req *api.StoreRequest[nam
6161
repo = *s.repo
6262
}
6363
se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...))
64-
if err != nil {
64+
var entityNotFoundError *ociremote.EntityNotFoundError
65+
if errors.As(err, &entityNotFoundError) {
66+
se = ociremote.SignedUnknown(req.Artifact)
67+
} else if err != nil {
6568
return nil, errors.Wrap(err, "getting signed image")
6669
}
6770

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2025 The Tekton Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package oci
16+
17+
import (
18+
"fmt"
19+
"net/http/httptest"
20+
"strings"
21+
"testing"
22+
23+
"github.com/google/go-containerregistry/pkg/name"
24+
"github.com/google/go-containerregistry/pkg/registry"
25+
"github.com/google/go-containerregistry/pkg/v1/random"
26+
"github.com/google/go-containerregistry/pkg/v1/remote"
27+
"github.com/google/go-containerregistry/pkg/v1/types"
28+
intoto "github.com/in-toto/attestation/go/v1"
29+
"github.com/tektoncd/chains/pkg/chains/signing"
30+
"github.com/tektoncd/chains/pkg/chains/storage/api"
31+
logtesting "knative.dev/pkg/logging/testing"
32+
)
33+
34+
func TestAttestationStorer_Store(t *testing.T) {
35+
tests := []struct {
36+
name string
37+
writeToRegistry func(*testing.T, string) name.Digest
38+
wantErr error
39+
}{
40+
{
41+
name: "image manifest",
42+
writeToRegistry: func(t *testing.T, registryName string) name.Digest {
43+
t.Helper()
44+
img, err := random.Image(1024, 2)
45+
if err != nil {
46+
t.Fatalf("failed to create random image: %s", err)
47+
}
48+
imgDigest, err := img.Digest()
49+
if err != nil {
50+
t.Fatalf("failed to get image digest: %v", err)
51+
}
52+
ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, imgDigest))
53+
if err != nil {
54+
t.Fatalf("failed to parse digest: %v", err)
55+
}
56+
if err := remote.Write(ref, img); err != nil {
57+
t.Fatalf("failed to write image to mock registry: %v", err)
58+
}
59+
return ref
60+
},
61+
},
62+
{
63+
name: "image layer",
64+
writeToRegistry: func(t *testing.T, registryName string) name.Digest {
65+
t.Helper()
66+
layer, err := random.Layer(1024, types.OCILayer)
67+
if err != nil {
68+
t.Fatalf("failed to create random layer: %v", err)
69+
}
70+
layerDigest, err := layer.Digest()
71+
if err != nil {
72+
t.Fatalf("failed to get layer digest: %v", err)
73+
}
74+
ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, layerDigest))
75+
if err != nil {
76+
t.Fatalf("failed to parse digest: %v", err)
77+
}
78+
if err := remote.WriteLayer(ref.Repository, layer); err != nil {
79+
t.Fatalf("failed to write layer to mock registry: %v", err)
80+
}
81+
return ref
82+
},
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
s := httptest.NewServer(registry.New())
89+
defer s.Close()
90+
registryName := strings.TrimPrefix(s.URL, "http://")
91+
92+
ref := tt.writeToRegistry(t, registryName)
93+
94+
storer, err := NewAttestationStorer(WithTargetRepository(ref.Repository))
95+
if err != nil {
96+
t.Fatalf("failed to create storer: %v", err)
97+
}
98+
99+
ctx := logtesting.TestContextWithLogger(t)
100+
_, err = storer.Store(ctx, &api.StoreRequest[name.Digest, *intoto.Statement]{
101+
Artifact: ref,
102+
Payload: &intoto.Statement{},
103+
Bundle: &signing.Bundle{},
104+
})
105+
106+
if err != nil {
107+
t.Fatalf("error during Store(): %s", err)
108+
}
109+
})
110+
}
111+
}

pkg/chains/storage/oci/simple.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ func (s *SimpleStorer) Store(ctx context.Context, req *api.StoreRequest[name.Dig
5757
logger.Info("Uploading signature")
5858

5959
se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...))
60-
if err != nil {
60+
var entityNotFoundError *ociremote.EntityNotFoundError
61+
if errors.As(err, &entityNotFoundError) {
62+
se = ociremote.SignedUnknown(req.Artifact)
63+
} else if err != nil {
6164
return nil, errors.Wrap(err, "getting signed image")
6265
}
6366

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2025 The Tekton Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package oci
16+
17+
import (
18+
"fmt"
19+
"net/http/httptest"
20+
"strings"
21+
"testing"
22+
23+
"github.com/google/go-containerregistry/pkg/name"
24+
"github.com/google/go-containerregistry/pkg/registry"
25+
"github.com/google/go-containerregistry/pkg/v1/random"
26+
"github.com/google/go-containerregistry/pkg/v1/remote"
27+
"github.com/google/go-containerregistry/pkg/v1/types"
28+
"github.com/tektoncd/chains/pkg/chains/formats/simple"
29+
"github.com/tektoncd/chains/pkg/chains/signing"
30+
"github.com/tektoncd/chains/pkg/chains/storage/api"
31+
logtesting "knative.dev/pkg/logging/testing"
32+
)
33+
34+
func TestSimpleStorer_Store(t *testing.T) {
35+
tests := []struct {
36+
name string
37+
writeToRegistry func(*testing.T, string) name.Digest
38+
}{
39+
{
40+
name: "image manifest",
41+
writeToRegistry: func(t *testing.T, registryName string) name.Digest {
42+
t.Helper()
43+
img, err := random.Image(1024, 2)
44+
if err != nil {
45+
t.Fatalf("failed to create random image: %s", err)
46+
}
47+
imgDigest, err := img.Digest()
48+
if err != nil {
49+
t.Fatalf("failed to get image digest: %v", err)
50+
}
51+
ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, imgDigest))
52+
if err != nil {
53+
t.Fatalf("failed to parse digest: %v", err)
54+
}
55+
if err := remote.Write(ref, img); err != nil {
56+
t.Fatalf("failed to write image to mock registry: %v", err)
57+
}
58+
return ref
59+
},
60+
},
61+
{
62+
name: "image layer",
63+
writeToRegistry: func(t *testing.T, registryName string) name.Digest {
64+
t.Helper()
65+
layer, err := random.Layer(1024, types.OCILayer)
66+
if err != nil {
67+
t.Fatalf("failed to create random layer: %s", err)
68+
}
69+
layerDigest, err := layer.Digest()
70+
if err != nil {
71+
t.Fatalf("failed to get layer digest: %v", err)
72+
}
73+
ref, err := name.NewDigest(fmt.Sprintf("%s/test/img@%s", registryName, layerDigest))
74+
if err != nil {
75+
t.Fatalf("failed to parse digest: %v", err)
76+
}
77+
if err := remote.WriteLayer(ref.Repository, layer); err != nil {
78+
t.Fatalf("failed to write layer to mock registry: %v", err)
79+
}
80+
return ref
81+
},
82+
},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
s := httptest.NewServer(registry.New())
88+
defer s.Close()
89+
registryName := strings.TrimPrefix(s.URL, "http://")
90+
91+
ref := tt.writeToRegistry(t, registryName)
92+
93+
storer, err := NewSimpleStorerFromConfig(WithTargetRepository(ref.Repository))
94+
if err != nil {
95+
t.Fatalf("failed to create storer: %v", err)
96+
}
97+
98+
ctx := logtesting.TestContextWithLogger(t)
99+
_, err = storer.Store(ctx, &api.StoreRequest[name.Digest, simple.SimpleContainerImage]{
100+
Artifact: ref,
101+
Payload: simple.NewSimpleStruct(ref),
102+
Bundle: &signing.Bundle{},
103+
})
104+
105+
if err != nil {
106+
t.Fatalf("error during Store(): %s", err)
107+
}
108+
})
109+
}
110+
}

vendor/github.com/google/go-containerregistry/pkg/v1/random/doc.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/google/go-containerregistry/pkg/v1/random/image.go

Lines changed: 116 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)