Skip to content

Commit

Permalink
Add -k8s flag to generate-key-pair (#345)
Browse files Browse the repository at this point in the history
* Add -k8s flag to generate-key-pair

This command will:
1. generate the key-pair in memory
2. prompt the user for a password
3. store cosign.pub, cosign.key and cosign.password as a Kubernetes secret
4. store cosign.pub to disk

Signed-off-by: Priya Wadhwa <[email protected]>

* Add integration test

Signed-off-by: Priya Wadhwa <[email protected]>

* make password private, expose via function

Signed-off-by: Priya Wadhwa <[email protected]>
  • Loading branch information
priyawadhwa authored Jun 2, 2021
1 parent 620c41d commit 8b0bcec
Show file tree
Hide file tree
Showing 8 changed files with 916 additions and 10 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ jobs:
run: |
curl -L https://github.com/google/ko/releases/download/v0.8.1/ko_0.8.1_Linux_x86_64.tar.gz | tar xzf - ko && \
chmod +x ./ko && sudo mv ko /usr/local/bin/
- name: setup kind cluster
run: |
go install sigs.k8s.io/[email protected]
kind create cluster
# Required for `make cosign-pivkey`
# - name: deps
# run: sudo apt-get update && sudo apt-get install -yq libpcsclite-dev
Expand Down
9 changes: 7 additions & 2 deletions cmd/cosign/cli/generate_key_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"golang.org/x/term"

"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/kubernetes"
"github.com/sigstore/sigstore/pkg/kms"
)

Expand All @@ -39,6 +40,7 @@ func GenerateKeyPair() *ffcli.Command {
var (
flagset = flag.NewFlagSet("cosign generate-key-pair", flag.ExitOnError)
kmsVal = flagset.String("kms", "", "create key pair in KMS service to use for signing")
k8sRef = flagset.String("k8s", "", "create key pair and store in Kubernetes secret, format as <namespace>/<secret name>")
)

return &ffcli.Command{
Expand All @@ -59,12 +61,12 @@ CAVEATS:
the COSIGN_PASSWORD environment variable to provide one.`,
FlagSet: flagset,
Exec: func(ctx context.Context, args []string) error {
return GenerateKeyPairCmd(ctx, *kmsVal)
return GenerateKeyPairCmd(ctx, *kmsVal, *k8sRef)
},
}
}

func GenerateKeyPairCmd(ctx context.Context, kmsVal string) error {
func GenerateKeyPairCmd(ctx context.Context, kmsVal, k8sRef string) error {
if kmsVal != "" {
k, err := kms.Get(ctx, kmsVal)
if err != nil {
Expand All @@ -84,6 +86,9 @@ func GenerateKeyPairCmd(ctx context.Context, kmsVal string) error {
fmt.Fprintln(os.Stderr, "Public key written to cosign.pub")
return nil
}
if k8sRef != "" {
return kubernetes.KeyPairSecret(k8sRef, GetPass)
}

keys, err := cosign.GenerateKeyPair(GetPass)
if err != nil {
Expand Down
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/sigstore/cosign
go 1.16

require (
github.com/GoogleContainerTools/skaffold v1.25.0
github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4
github.com/go-openapi/analysis v0.20.1 // indirect
github.com/go-openapi/runtime v0.19.28
Expand All @@ -11,6 +12,7 @@ require (
github.com/go-piv/piv-go v1.7.0
github.com/google/go-cmp v0.5.6
github.com/google/go-containerregistry v0.5.1
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/trillian v1.3.14-0.20210413093047-5e12fb368c8f
github.com/jedisct1/go-minisign v0.0.0-20210414164026-819d7e2534ac // indirect
github.com/leodido/go-urn v1.2.1 // indirect
Expand All @@ -20,6 +22,7 @@ require (
github.com/pelletier/go-toml v1.9.1 // indirect
github.com/peterbourgon/ff/v3 v3.0.0
github.com/pkg/errors v0.9.1
github.com/prometheus/common v0.25.0 // indirect
github.com/sigstore/fulcio v0.0.0-20210405115948-e7630f533fca
github.com/sigstore/rekor v0.1.2-0.20210519014330-b5480728bde6
github.com/sigstore/sigstore v0.0.0-20210530211317-99216b8b86a6
Expand All @@ -29,11 +32,13 @@ require (
go.mongodb.org/mongo-driver v1.5.2 // indirect
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f // indirect
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
google.golang.org/genproto v0.0.0-20210521181308-5ccab8a35a9a // indirect
google.golang.org/grpc v1.38.0 // indirect
k8s.io/api v0.19.7
k8s.io/apimachinery v0.21.1
)
692 changes: 685 additions & 7 deletions go.sum

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pkg/cosign/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type PassFunc func(bool) ([]byte, error)
type Keys struct {
PrivateBytes []byte
PublicBytes []byte
password []byte
}

func GeneratePrivateKey() (*ecdsa.PrivateKey, error) {
Expand Down Expand Up @@ -91,9 +92,14 @@ func GenerateKeyPair(pf PassFunc) (*Keys, error) {
return &Keys{
PrivateBytes: privBytes,
PublicBytes: pubBytes,
password: password,
}, nil
}

func (k *Keys) Password() []byte {
return k.password
}

func PublicKeyPem(ctx context.Context, key signature.PublicKeyProvider) ([]byte, error) {
pub, err := key.PublicKey(ctx)
if err != nil {
Expand Down
86 changes: 86 additions & 0 deletions pkg/cosign/kubernetes/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 kubernetes

import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"

kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
"github.com/pkg/errors"
"github.com/sigstore/cosign/pkg/cosign"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func KeyPairSecret(k8sRef string, pf cosign.PassFunc) error {
namespace, name, err := parseRef(k8sRef)
if err != nil {
return err
}
// now, generate the key in memory
keys, err := cosign.GenerateKeyPair(pf)
if err != nil {
return errors.Wrap(err, "generating key pair")
}

ctx := context.TODO()
// create the client
client, err := kubernetesclient.Client()
if err != nil {
return errors.Wrap(err, "new for config")
}
s, err := client.CoreV1().Secrets(namespace).Create(ctx, secret(keys, namespace, name), metav1.CreateOptions{})
if err != nil {
return errors.Wrapf(err, "creating secret %s in ns %s", name, namespace)
}

fmt.Fprintf(os.Stderr, "Successfully created secret %s in namespace %s\n", s.Name, s.Namespace)
if err := ioutil.WriteFile("cosign.pub", keys.PublicBytes, 0600); err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Public key written to cosign.pub")
return nil
}

// creates a secret with the following data:
// * cosign.key
// * cosign.pub
// * cosign.password
func secret(keys *cosign.Keys, namespace, name string) *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string][]byte{
"cosign.key": keys.PrivateBytes,
"cosign.pub": keys.PublicBytes,
"cosign.password": keys.Password(),
},
}
}

// the reference should be formatted as <namespace>/<secret name>
func parseRef(k8sRef string) (string, string, error) {
s := strings.Split(k8sRef, "/")
if len(s) != 2 {
return "", "", errors.New("please format the k8s secret reference as <namespace>/<secret name>")
}
return s[0], s[1], nil
}
90 changes: 90 additions & 0 deletions pkg/cosign/kubernetes/secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 kubernetes

import (
"reflect"
"testing"

"github.com/sigstore/cosign/pkg/cosign"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestSecret(t *testing.T) {
keys := &cosign.Keys{
PrivateBytes: []byte("private"),
PublicBytes: []byte("public"),
}
name := "secret"
namespace := "default"
expect := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
},
Data: map[string][]byte{
"cosign.key": []byte("private"),
"cosign.pub": []byte("public"),
"cosign.password": nil,
},
}
actual := secret(keys, namespace, name)
if !reflect.DeepEqual(actual, expect) {
t.Errorf("secret: %v, want %v", expect, actual)
}
}

func TestParseRef(t *testing.T) {
tests := []struct {
desc string
ref string
name string
namespace string
shouldErr bool
}{
{
desc: "valid",
ref: "default/cosign-secret",
name: "cosign-secret",
namespace: "default",
}, {
desc: "invalid, 1 field",
ref: "something",
shouldErr: true,
}, {
desc: "invalid, more than 2 fields",
ref: "yet/another/arg",
shouldErr: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
ns, name, err := parseRef(test.ref)
if (err == nil) == test.shouldErr {
t.Fatal("unexpected error")
}
if test.shouldErr {
return
}
if name != test.name {
t.Fatalf("unexpected name: got %v expected %v", name, test.name)
}
if ns != test.namespace {
t.Fatalf("unexpected name: got %v expected %v", ns, test.namespace)
}
})
}
}
32 changes: 32 additions & 0 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http/httptest"
"net/url"
Expand All @@ -40,8 +41,12 @@ import (
"github.com/sigstore/cosign/cmd/cosign/cli"
sget "github.com/sigstore/cosign/cmd/sget/cli"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/kubernetes"
cremote "github.com/sigstore/cosign/pkg/cosign/remote"
"github.com/sigstore/sigstore/pkg/signature/payload"

kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var keyPass = []byte("hello")
Expand Down Expand Up @@ -223,6 +228,33 @@ func TestGenerateKeyPairEnvVar(t *testing.T) {
}
}

func TestGenerateKeyPairK8s(t *testing.T) {
td := t.TempDir()
if err := os.Chdir(td); err != nil {
t.Fatal(err)
}
password := "foo"
defer setenv(t, "COSIGN_PASSWORD", password)()
ctx := context.Background()
name := "cosign-secret"
namespace := "default"
if err := kubernetes.KeyPairSecret(fmt.Sprintf("%s/%s", namespace, name), cli.GetPass); err != nil {
t.Fatal(err)
}
// make sure the secret actually exists
client, err := kubernetesclient.Client()
if err != nil {
t.Fatal(err)
}
s, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if v, ok := s.Data["cosign.password"]; !ok || string(v) != password {
t.Fatalf("password is incorrect, got %v expected %v", v, "foo")
}
}

func TestMultipleSignatures(t *testing.T) {
repo, stop := reg(t)
defer stop()
Expand Down

0 comments on commit 8b0bcec

Please sign in to comment.