Skip to content

Commit

Permalink
Sign without pulling from the registry
Browse files Browse the repository at this point in the history
This allows signing in a disconnected environment.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
  • Loading branch information
sudo-bmitch committed Oct 15, 2021
1 parent 7d2d51d commit 8c4dc6c
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 56 deletions.
136 changes: 80 additions & 56 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/sigstore/cosign/pkg/cosign/pivkey"
cremote "github.com/sigstore/cosign/pkg/cosign/remote"
"github.com/sigstore/cosign/pkg/oci"
ociempty "github.com/sigstore/cosign/pkg/oci/empty"
"github.com/sigstore/cosign/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/pkg/oci/static"
Expand Down Expand Up @@ -168,6 +169,18 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a
return fmt.Errorf("unable to resolve attachment %s for image %s", attachment, inputImg)
}

if digest, ok := ref.(name.Digest); ok && !recursive {
se, err := ociempty.SignedImage(ref)
if err != nil {
return err
}
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se)
if err != nil {
return err
}
continue
}

se, err := ociremote.SignedEntity(ref, opts...)
if err != nil {
return err
Expand All @@ -181,74 +194,85 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a
}
digest := ref.Context().Digest(d.String())

// The payload can be specified via a flag to skip generation.
payload := staticPayload
if len(payload) == 0 {
payload, err = (&sigPayload.Cosign{
Image: digest,
Annotations: annotations,
}).MarshalJSON()
if err != nil {
return errors.Wrap(err, "payload")
}
}

signature, err := sv.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx))
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se)
if err != nil {
return errors.Wrap(err, "signing")
}
b64sig := base64.StdEncoding.EncodeToString(signature)

if !upload {
fmt.Println(b64sig)
return ErrDone
return err
}
return ErrDone
}); err != nil {
return err
}
}

opts := []static.Option{}
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}
return nil
}

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

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

// Attach the signature to the entity.
newSE, err := mutate.AttachSignatureToEntity(se, sig, mutate.WithDupeDetector(dd))
if err != nil {
return err
}
if !upload {
fmt.Println(b64sig)
return nil
}

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

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

// Create the new signature for this entity.
sig, err := static.NewSignature(payload, b64sig, opts...)
if err != nil {
return err
}

// Attach the signature to the entity.
newSE, err := mutate.AttachSignatureToEntity(se, sig, mutate.WithDupeDetector(dd))
if err != nil {
return err
}

// Publish the signatures associated with this entity
walkOpts, err := regOpts.ClientOpts(ctx)
if err != nil {
return errors.Wrap(err, "constructing client options")
}

// Publish the signatures associated with this entity
if err := ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...); err != nil {
return err
}
return nil
}

Expand Down
65 changes: 65 additions & 0 deletions pkg/oci/empty/signed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Copyright 2021 The Sigstore 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 empty

import (
"fmt"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/sigstore/cosign/pkg/oci"
)

type signedImage struct {
v1.Image
digest v1.Hash
signature oci.Signatures
attestations oci.Signatures
}

func (se *signedImage) Signatures() (oci.Signatures, error) {
return se.signature, nil
}

func (se *signedImage) Attestations() (oci.Signatures, error) {
return se.attestations, nil
}

func (se *signedImage) Digest() (v1.Hash, error) {
if se.digest.Hex == "" {
return v1.Hash{}, fmt.Errorf("digest not available")
}
return se.digest, nil
}

func SignedImage(ref name.Reference) (oci.SignedImage, error) {
var err error
d := v1.Hash{}
base := empty.Image
if digest, ok := ref.(name.Digest); ok {
d, err = v1.NewHash(digest.DigestStr())
if err != nil {
return nil, err
}
}
return &signedImage{
Image: base,
digest: d,
signature: Signatures(),
attestations: Signatures(),
}, nil
}
68 changes: 68 additions & 0 deletions pkg/oci/empty/signed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// Copyright 2021 The Sigstore 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 empty

import (
"testing"

"github.com/google/go-containerregistry/pkg/name"
)

func TestSignedImage(t *testing.T) {
tests := []struct {
ref string
digestStr string
digestErr string
}{
{
ref: "hello-world:latest",
digestErr: "digest not available",
},
{
ref: "hello-world@sha256:e1c082e3d3c45cccac829840a25941e679c25d438cc8412c2fa221cf1a824e6a",
digestStr: "sha256:e1c082e3d3c45cccac829840a25941e679c25d438cc8412c2fa221cf1a824e6a",
},
}
for _, test := range tests {
ref, err := name.ParseReference(test.ref)
if err != nil {
t.Errorf("failed to parse ref \"%s\": %v", test.ref, err)
continue
}
se, err := SignedImage(ref)
if err != nil {
t.Errorf("failed to create signed image for \"%s\": %v", test.ref, err)
continue
}
d, err := se.Digest()
if (err == nil && test.digestErr != "") ||
(err != nil && test.digestErr == "") ||
(err != nil && test.digestErr != "" && err.Error() != test.digestErr) {
t.Errorf("digest error mismatch for \"%s\": expected %s, saw %v", test.ref, test.digestErr, err)
}
if test.digestStr != "" && d.String() != test.digestStr {
t.Errorf("digest mismatch for \"%s\": expected %s, saw %s", test.ref, test.digestStr, d.String())
}
_, err = se.Signatures()
if err != nil {
t.Errorf("failed to get signatures for %s: %v", test.ref, err)
}
_, err = se.Attestations()
if err != nil {
t.Errorf("failed to get attestations for %s: %v", test.ref, err)
}
}
}

0 comments on commit 8c4dc6c

Please sign in to comment.