From 2522ba4db813e698ce8da6854d4d5dbafd813043 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Thu, 25 Apr 2019 15:24:17 -0400 Subject: [PATCH] Passing `--to-signature` to release new creates the signature file The signature file can be signed by an atomic container image signer as an OpenPGP message in order to verify update payloads. --- contrib/completions/bash/oc | 2 + contrib/completions/zsh/oc | 2 + pkg/oc/cli/admin/release/new.go | 22 ++++++++ pkg/oc/cli/admin/release/signature.go | 72 +++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 pkg/oc/cli/admin/release/signature.go diff --git a/contrib/completions/bash/oc b/contrib/completions/bash/oc index 0fe66642b8d0..5c72762d9a44 100644 --- a/contrib/completions/bash/oc +++ b/contrib/completions/bash/oc @@ -4458,6 +4458,8 @@ _oc_adm_release_new() local_nonpersistent_flags+=("--to-image-base=") flags+=("--to-image-base-tag=") local_nonpersistent_flags+=("--to-image-base-tag=") + flags+=("--to-signature=") + local_nonpersistent_flags+=("--to-signature=") flags+=("--as=") flags+=("--as-group=") flags+=("--cache-dir=") diff --git a/contrib/completions/zsh/oc b/contrib/completions/zsh/oc index 382b58c00579..eb85e2388e10 100644 --- a/contrib/completions/zsh/oc +++ b/contrib/completions/zsh/oc @@ -4600,6 +4600,8 @@ _oc_adm_release_new() local_nonpersistent_flags+=("--to-image-base=") flags+=("--to-image-base-tag=") local_nonpersistent_flags+=("--to-image-base-tag=") + flags+=("--to-signature=") + local_nonpersistent_flags+=("--to-signature=") flags+=("--as=") flags+=("--as-group=") flags+=("--cache-dir=") diff --git a/pkg/oc/cli/admin/release/new.go b/pkg/oc/cli/admin/release/new.go index 902436cc5e05..2e425647f814 100644 --- a/pkg/oc/cli/admin/release/new.go +++ b/pkg/oc/cli/admin/release/new.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/pkg/version" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/util/templates" @@ -145,6 +146,7 @@ func NewRelease(f kcmdutil.Factory, parentName string, streams genericclioptions flags.StringVar(&o.ToImage, "to-image", o.ToImage, "The location to upload the release image to.") flags.StringVar(&o.ToImageBase, "to-image-base", o.ToImageBase, "If specified, the image to add the release layer on top of.") flags.StringVar(&o.ToImageBaseTag, "to-image-base-tag", o.ToImageBaseTag, "If specified, the image tag in the input to add the release layer on top of. Defaults to cluster-version-operator.") + flags.StringVar(&o.ToSignature, "to-signature", o.ToSignature, "If specified, output a message that can be signed that describes this release. Requires --to-image.") // misc flags.StringVarP(&o.Output, "output", "o", o.Output, "Output the mapping definition in this format.") @@ -189,6 +191,7 @@ type NewOptions struct { ToImage string ToImageBase string ToImageBaseTag string + ToSignature string Mirror string @@ -262,6 +265,9 @@ func (o *NewOptions) Validate() error { return fmt.Errorf("must specify image mappings when no other source is defined") } } + if len(o.ToSignature) > 0 && len(o.ToImage) == 0 { + return fmt.Errorf("--to-signature requires --to-image") + } if len(o.Mirror) > 0 && o.ReferenceMode != "" && o.ReferenceMode != "public" { return fmt.Errorf("--reference-mode must be public or empty when using --mirror") } @@ -1160,6 +1166,22 @@ func (o *NewOptions) write(r io.Reader, is *imageapi.ImageStream, now time.Time) fmt.Fprintf(o.ErrOut, "warning: %v\n", err) } + // TODO: support a dry run that doesn't push the image, but requires append to have a dry-run mode + toRefWithDigest := toRef + toRefWithDigest.Tag = "" + toRefWithDigest.ID = options.ToDigest.String() + msg, err := createReleaseSignatureMessage(fmt.Sprintf("oc-adm-release-new/%s", version.Get().GitCommit), now, options.ToDigest.String(), toRefWithDigest.Exact()) + if err != nil { + return err + } + if len(o.ToSignature) > 0 { + if err := ioutil.WriteFile(o.ToSignature, msg, 0644); err != nil { + return fmt.Errorf("unable to write signature file: %v", err) + } + } else { + klog.V(2).Infof("Signature for output:\n%s", string(msg)) + } + default: fmt.Fprintf(o.ErrOut, "info: Extracting operator contents to disk without building a release artifact\n") if _, err := io.Copy(ioutil.Discard, r); err != nil { diff --git a/pkg/oc/cli/admin/release/signature.go b/pkg/oc/cli/admin/release/signature.go new file mode 100644 index 000000000000..d26f52fd5384 --- /dev/null +++ b/pkg/oc/cli/admin/release/signature.go @@ -0,0 +1,72 @@ +package release + +import ( + "encoding/json" + "fmt" + "time" +) + +// createReleaseSignatureMessage creates the core message to sign the release payload. +func createReleaseSignatureMessage(signer string, now time.Time, releaseDigest, pullSpec string) ([]byte, error) { + if len(signer) == 0 || now.IsZero() || len(releaseDigest) == 0 || len(pullSpec) == 0 { + return nil, fmt.Errorf("you must specify a signer, current timestamp, release digest, and pull spec to sign") + } + + sig := &signature{ + Critical: criticalSignature{ + Type: "atomic container signature", + Image: criticalImage{ + DockerManifestDigest: releaseDigest, + }, + Identity: criticalIdentity{ + DockerReference: pullSpec, + }, + }, + Optional: optionalSignature{ + Creator: signer, + Timestamp: now.Unix(), + }, + } + return json.MarshalIndent(sig, "", " ") +} + +// An atomic container signature has the following schema: +// +// { +// "critical": { +// "type": "atomic container signature", +// "image": { +// "docker-manifest-digest": "sha256:817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e" +// }, +// "identity": { +// "docker-reference": "docker.io/library/busybox:latest" +// } +// }, +// "optional": { +// "creator": "some software package v1.0.1-35", +// "timestamp": 1483228800, +// } +// } +type signature struct { + Critical criticalSignature `json:"critical"` + Optional optionalSignature `json:"optional"` +} + +type criticalSignature struct { + Type string `json:"type"` + Image criticalImage `json:"image"` + Identity criticalIdentity `json:"identity"` +} + +type criticalImage struct { + DockerManifestDigest string `json:"docker-manifest-digest"` +} + +type criticalIdentity struct { + DockerReference string `json:"docker-reference"` +} + +type optionalSignature struct { + Creator string `json:"creator"` + Timestamp int64 `json:"timestamp"` +}