Skip to content

Commit 42e5df0

Browse files
authored
Sign without pulling from the registry (#903)
This allows signing in a disconnected environment. Signed-off-by: Brandon Mitchell <[email protected]>
1 parent 7d2d51d commit 42e5df0

File tree

3 files changed

+213
-56
lines changed

3 files changed

+213
-56
lines changed

cmd/cosign/cli/sign/sign.go

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"github.com/sigstore/cosign/pkg/cosign/pivkey"
4343
cremote "github.com/sigstore/cosign/pkg/cosign/remote"
4444
"github.com/sigstore/cosign/pkg/oci"
45+
ociempty "github.com/sigstore/cosign/pkg/oci/empty"
4546
"github.com/sigstore/cosign/pkg/oci/mutate"
4647
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
4748
"github.com/sigstore/cosign/pkg/oci/static"
@@ -168,6 +169,18 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a
168169
return fmt.Errorf("unable to resolve attachment %s for image %s", attachment, inputImg)
169170
}
170171

172+
if digest, ok := ref.(name.Digest); ok && !recursive {
173+
se, err := ociempty.SignedImage(ref)
174+
if err != nil {
175+
return err
176+
}
177+
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se)
178+
if err != nil {
179+
return err
180+
}
181+
continue
182+
}
183+
171184
se, err := ociremote.SignedEntity(ref, opts...)
172185
if err != nil {
173186
return err
@@ -181,74 +194,85 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a
181194
}
182195
digest := ref.Context().Digest(d.String())
183196

184-
// The payload can be specified via a flag to skip generation.
185-
payload := staticPayload
186-
if len(payload) == 0 {
187-
payload, err = (&sigPayload.Cosign{
188-
Image: digest,
189-
Annotations: annotations,
190-
}).MarshalJSON()
191-
if err != nil {
192-
return errors.Wrap(err, "payload")
193-
}
194-
}
195-
196-
signature, err := sv.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx))
197+
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se)
197198
if err != nil {
198-
return errors.Wrap(err, "signing")
199-
}
200-
b64sig := base64.StdEncoding.EncodeToString(signature)
201-
202-
if !upload {
203-
fmt.Println(b64sig)
204-
return ErrDone
199+
return err
205200
}
201+
return ErrDone
202+
}); err != nil {
203+
return err
204+
}
205+
}
206206

207-
opts := []static.Option{}
208-
if sv.Cert != nil {
209-
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
210-
}
207+
return nil
208+
}
211209

212-
// Check whether we should be uploading to the transparency log
213-
if uploadTLog, err := ShouldUploadToTlog(digest, force, ko.RekorURL); err != nil {
214-
return err
215-
} else if uploadTLog {
216-
bundle, err := UploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
217-
return cosign.TLogUpload(r, signature, payload, b)
218-
})
219-
if err != nil {
220-
return err
221-
}
222-
opts = append(opts, static.WithBundle(bundle))
223-
}
210+
func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyOpts,
211+
regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, force bool,
212+
dd mutate.DupeDetector, sv *CertSignVerifier, se oci.SignedEntity) error {
213+
var err error
214+
// The payload can be passed to skip generation.
215+
if len(payload) == 0 {
216+
payload, err = (&sigPayload.Cosign{
217+
Image: digest,
218+
Annotations: annotations,
219+
}).MarshalJSON()
220+
if err != nil {
221+
return errors.Wrap(err, "payload")
222+
}
223+
}
224224

225-
// Create the new signature for this entity.
226-
sig, err := static.NewSignature(payload, b64sig, opts...)
227-
if err != nil {
228-
return err
229-
}
225+
signature, err := sv.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx))
226+
if err != nil {
227+
return errors.Wrap(err, "signing")
228+
}
229+
b64sig := base64.StdEncoding.EncodeToString(signature)
230230

231-
// Attach the signature to the entity.
232-
newSE, err := mutate.AttachSignatureToEntity(se, sig, mutate.WithDupeDetector(dd))
233-
if err != nil {
234-
return err
235-
}
231+
if !upload {
232+
fmt.Println(b64sig)
233+
return nil
234+
}
236235

237-
walkOpts, err := regOpts.ClientOpts(ctx)
238-
if err != nil {
239-
return errors.Wrap(err, "constructing client options")
240-
}
236+
opts := []static.Option{}
237+
if sv.Cert != nil {
238+
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
239+
}
241240

242-
// Publish the signatures associated with this entity
243-
if err := ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...); err != nil {
244-
return err
245-
}
246-
return ErrDone
247-
}); err != nil {
241+
// Check whether we should be uploading to the transparency log
242+
if uploadTLog, err := ShouldUploadToTlog(digest, force, ko.RekorURL); err != nil {
243+
return err
244+
} else if uploadTLog {
245+
bundle, err := UploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
246+
return cosign.TLogUpload(r, signature, payload, b)
247+
})
248+
if err != nil {
248249
return err
249250
}
251+
opts = append(opts, static.WithBundle(bundle))
250252
}
251253

254+
// Create the new signature for this entity.
255+
sig, err := static.NewSignature(payload, b64sig, opts...)
256+
if err != nil {
257+
return err
258+
}
259+
260+
// Attach the signature to the entity.
261+
newSE, err := mutate.AttachSignatureToEntity(se, sig, mutate.WithDupeDetector(dd))
262+
if err != nil {
263+
return err
264+
}
265+
266+
// Publish the signatures associated with this entity
267+
walkOpts, err := regOpts.ClientOpts(ctx)
268+
if err != nil {
269+
return errors.Wrap(err, "constructing client options")
270+
}
271+
272+
// Publish the signatures associated with this entity
273+
if err := ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...); err != nil {
274+
return err
275+
}
252276
return nil
253277
}
254278

pkg/oci/empty/signed.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Copyright 2021 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package empty
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/google/go-containerregistry/pkg/name"
22+
v1 "github.com/google/go-containerregistry/pkg/v1"
23+
"github.com/google/go-containerregistry/pkg/v1/empty"
24+
"github.com/sigstore/cosign/pkg/oci"
25+
)
26+
27+
type signedImage struct {
28+
v1.Image
29+
digest v1.Hash
30+
signature oci.Signatures
31+
attestations oci.Signatures
32+
}
33+
34+
func (se *signedImage) Signatures() (oci.Signatures, error) {
35+
return se.signature, nil
36+
}
37+
38+
func (se *signedImage) Attestations() (oci.Signatures, error) {
39+
return se.attestations, nil
40+
}
41+
42+
func (se *signedImage) Digest() (v1.Hash, error) {
43+
if se.digest.Hex == "" {
44+
return v1.Hash{}, fmt.Errorf("digest not available")
45+
}
46+
return se.digest, nil
47+
}
48+
49+
func SignedImage(ref name.Reference) (oci.SignedImage, error) {
50+
var err error
51+
d := v1.Hash{}
52+
base := empty.Image
53+
if digest, ok := ref.(name.Digest); ok {
54+
d, err = v1.NewHash(digest.DigestStr())
55+
if err != nil {
56+
return nil, err
57+
}
58+
}
59+
return &signedImage{
60+
Image: base,
61+
digest: d,
62+
signature: Signatures(),
63+
attestations: Signatures(),
64+
}, nil
65+
}

pkg/oci/empty/signed_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Copyright 2021 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package empty
17+
18+
import (
19+
"testing"
20+
21+
"github.com/google/go-containerregistry/pkg/name"
22+
)
23+
24+
func TestSignedImage(t *testing.T) {
25+
tests := []struct {
26+
ref string
27+
digestStr string
28+
digestErr string
29+
}{
30+
{
31+
ref: "hello-world:latest",
32+
digestErr: "digest not available",
33+
},
34+
{
35+
ref: "hello-world@sha256:e1c082e3d3c45cccac829840a25941e679c25d438cc8412c2fa221cf1a824e6a",
36+
digestStr: "sha256:e1c082e3d3c45cccac829840a25941e679c25d438cc8412c2fa221cf1a824e6a",
37+
},
38+
}
39+
for _, test := range tests {
40+
ref, err := name.ParseReference(test.ref)
41+
if err != nil {
42+
t.Errorf("failed to parse ref \"%s\": %v", test.ref, err)
43+
continue
44+
}
45+
se, err := SignedImage(ref)
46+
if err != nil {
47+
t.Errorf("failed to create signed image for \"%s\": %v", test.ref, err)
48+
continue
49+
}
50+
d, err := se.Digest()
51+
if (err == nil && test.digestErr != "") ||
52+
(err != nil && test.digestErr == "") ||
53+
(err != nil && test.digestErr != "" && err.Error() != test.digestErr) {
54+
t.Errorf("digest error mismatch for \"%s\": expected %s, saw %v", test.ref, test.digestErr, err)
55+
}
56+
if test.digestStr != "" && d.String() != test.digestStr {
57+
t.Errorf("digest mismatch for \"%s\": expected %s, saw %s", test.ref, test.digestStr, d.String())
58+
}
59+
_, err = se.Signatures()
60+
if err != nil {
61+
t.Errorf("failed to get signatures for %s: %v", test.ref, err)
62+
}
63+
_, err = se.Attestations()
64+
if err != nil {
65+
t.Errorf("failed to get attestations for %s: %v", test.ref, err)
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)