diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index 13d6d26414d..5ec5576eb18 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -124,6 +124,7 @@ func New() *cobra.Command { // Add sub-commands. addPublicKey(cmd) + addPolicy(cmd) addGenerate(cmd) addSign(cmd) addSignBlob(cmd) diff --git a/cmd/cosign/cli/options/policy.go b/cmd/cosign/cli/options/policy.go new file mode 100644 index 00000000000..2e80f9720fd --- /dev/null +++ b/cmd/cosign/cli/options/policy.go @@ -0,0 +1,45 @@ +// +// 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 options + +import ( + "github.com/spf13/cobra" +) + +// PolicyInitOptions is the top level wrapper for the policy-init command. +type PolicyInitOptions struct { + ImageRef string + Maintainers []string + Threshold int + OutFile string +} + +var _ Interface = (*PolicyInitOptions)(nil) + +// AddFlags implements Interface +func (o *PolicyInitOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.ImageRef, "namespace", "ns", + "registry namespace that the root policy belongs to") + + cmd.Flags().StringVar(&o.OutFile, "out", "o", + "output policy locally") + + cmd.Flags().IntVar(&o.Threshold, "threshold", 1, + "threshold for root policy signers") + + cmd.Flags().StringSliceVarP(&o.Maintainers, "maintainers", "m", nil, + "list of maintainers to add to the root policy") +} diff --git a/cmd/cosign/cli/policy_init.go b/cmd/cosign/cli/policy_init.go new file mode 100644 index 00000000000..e0170072fc0 --- /dev/null +++ b/cmd/cosign/cli/policy_init.go @@ -0,0 +1,126 @@ +// +// 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 cli + +import ( + "fmt" + "io/ioutil" + "net/mail" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/upload" + cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/spf13/cobra" +) + +func validEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil +} + +func addPolicy(topLevel *cobra.Command) { + o := &options.PolicyInitOptions{} + + policyCmd := &cobra.Command{ + Use: "policy", + Short: "subcommand to manage a keyless policy.", + Long: "policy is used to manage a root.json policy\nfor keyless signing delegation. This is used to establish a policy for a registry namespace,\na signing threshold and a list of maintainers who can sign over the body section.", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + + initCmd := &cobra.Command{ + Use: "init", + Short: "generate a new keyless policy.", + Long: "init is used to generate a root.json policy\nfor keyless signing delegation. This is used to establish a policy for a registry namespace,\na signing threshold and a list of maintainers who can sign over the body section.", + Example: ` + # extract public key from private key to a specified out file. + cosign policy init -ns --maintainers {email_addresses} --threshold --expires (days)`, + RunE: func(cmd *cobra.Command, args []string) error { + var publicKeys []*tuf.Key + + // Process the list of maintainers by + // 1. Ensure each entry is a correctly formatted email address + // 2. If 1 is true, then remove surplus whitespace (caused by gaps between commas) + for _, email := range o.Maintainers { + if !validEmail(email) { + panic(fmt.Sprintf("Invalid email format: %s", email)) + } else { + // Currently no issuer is set: this would need to be set by the initializer. + key := tuf.FulcioVerificationKey(strings.TrimSpace(email), "") + publicKeys = append(publicKeys, key) + } + } + + // Create a new root. + root := tuf.NewRoot() + + // Add the maintainer identities to the root's trusted keys. + for _, key := range publicKeys { + root.AddKey(key) + } + + // Set root keys, threshold, and namespace. + role, ok := root.Roles["root"] + if !ok { + role = &tuf.Role{KeyIDs: []string{}, Threshold: 1} + } + role.AddKeysWithThreshold(publicKeys, o.Threshold) + root.Roles["root"] = role + root.Namespace = o.ImageRef + + policy, err := root.Marshal() + if err != nil { + return err + } + policyFile, err := policy.JSONMarshal("", "\t") + if err != nil { + return err + } + + var outfile string + if o.OutFile != "" { + outfile = o.OutFile + err = ioutil.WriteFile(o.OutFile, policyFile, 0600) + if err != nil { + return errors.Wrapf(err, "error writing to root.json") + } + } else { + tempFile, err := os.CreateTemp("", "root") + if err != nil { + return err + } + outfile = tempFile.Name() + defer os.Remove(tempFile.Name()) + } + + files := []cremote.File{ + cremote.FileFromFlag(outfile), + } + + return upload.BlobCmd(cmd.Context(), options.RegistryOptions{}, files, "", o.ImageRef+"/root.json") + }, + } + + o.AddFlags(initCmd) + policyCmd.AddCommand(initCmd) + topLevel.AddCommand(policyCmd) +} diff --git a/cmd/cosign/cli/policy_init_test.go b/cmd/cosign/cli/policy_init_test.go new file mode 100644 index 00000000000..1d0fbea850f --- /dev/null +++ b/cmd/cosign/cli/policy_init_test.go @@ -0,0 +1,33 @@ +// +// 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 cli + +import ( + "testing" +) + +// Tests correctly formatted emails do not fail validEmail call +// Tests incorrectly formatted emails do not pass validEmail call +func TestEmailValid(t *testing.T) { + goodEmail := "foo@foo.com" + strongBadEmail := "foofoocom" + + if !validEmail(goodEmail) { + t.Errorf("correct email %s, failed valid check", goodEmail) + } else if validEmail(strongBadEmail) { + t.Errorf("bad email %s, passed valid check", strongBadEmail) + } +} diff --git a/cmd/cosign/main.go b/cmd/cosign/main.go index 559106c8ec6..62274a03989 100644 --- a/cmd/cosign/main.go +++ b/cmd/cosign/main.go @@ -49,7 +49,7 @@ func main() { // escape the remaining args to let them be passed to cobra. if len(os.Args) > 1 { switch os.Args[1] { - case "public-key", "generate-key-pair", + case "public-key", "policy", "generate-key-pair", "generate", "sign", "sign-blob", "upload", "piv-tool", diff --git a/go.mod b/go.mod index aca0c6dfbaf..53569f6b961 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.1.0 github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cobra v1.2.1 + github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 github.com/urfave/cli v1.22.5 // indirect go.opentelemetry.io/contrib v0.22.0 // indirect go.opentelemetry.io/proto/otlp v0.9.0 // indirect diff --git a/pkg/cosign/tuf/policy.go b/pkg/cosign/tuf/policy.go new file mode 100644 index 00000000000..22d55947106 --- /dev/null +++ b/pkg/cosign/tuf/policy.go @@ -0,0 +1,148 @@ +// +// 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. + +// Contains root policy definitions. +// Eventually, this will move this to go-tuf definitions. + +package tuf + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "sync" + "time" + + cjson "github.com/tent/canonical-json-go" +) + +type Signed struct { + Signed json.RawMessage `json:"signed"` + Signatures []Signature `json:"signatures"` +} + +type Signature struct { + KeyID string `json:"keyid"` + Signature string `json:"sig"` + Cert string `json:"cert,omitempty"` +} + +type Key struct { + Type string `json:"keytype"` + Scheme string `json:"scheme"` + Algorithms []string `json:"keyid_hash_algorithms,omitempty"` + Value json.RawMessage `json:"keyval"` + + id string + idOnce sync.Once +} + +func (k *Key) ID() string { + k.idOnce.Do(func() { + data, _ := cjson.Marshal(k) + digest := sha256.Sum256(data) + k.id = hex.EncodeToString(digest[:]) + }) + return k.id +} + +func (k *Key) ContainsID(id string) bool { + return id == k.ID() +} + +type Root struct { + Type string `json:"_type"` + SpecVersion string `json:"spec_version"` + Version int `json:"version"` + Expires time.Time `json:"expires"` + Keys map[string]*Key `json:"keys"` + Roles map[string]*Role `json:"roles"` + Namespace string `json:"namespace"` + + ConsistentSnapshot bool `json:"consistent_snapshot"` +} + +func DefaultExpires(role string) time.Time { + // Default expires in 3 months + return time.Now().AddDate(0, 3, 0).UTC().Round(time.Second) +} + +func NewRoot() *Root { + return &Root{ + Type: "root", + SpecVersion: "1.0", + Version: 1, + Expires: DefaultExpires("root"), + Keys: make(map[string]*Key), + Roles: make(map[string]*Role), + ConsistentSnapshot: true, + } +} + +func (r *Root) AddKey(key *Key) bool { + changed := false + if _, ok := r.Keys[key.ID()]; !ok { + changed = true + r.Keys[key.ID()] = key + } + + return changed +} + +type Role struct { + KeyIDs []string `json:"keyids"` + Threshold int `json:"threshold"` +} + +func (r *Role) AddKeysWithThreshold(keys []*Key, threshold int) bool { + roleIDs := make(map[string]struct{}) + for _, id := range r.KeyIDs { + roleIDs[id] = struct{}{} + } + changed := false + for _, key := range keys { + if _, ok := roleIDs[key.ID()]; !ok { + changed = true + r.KeyIDs = append(r.KeyIDs, key.ID()) + } + } + r.Threshold = threshold + return changed +} + +func (r *Root) Marshal() (*Signed, error) { + // Marshals the Root into a Signed type + b, err := cjson.Marshal(r) + if err != nil { + return nil, err + } + return &Signed{Signed: b}, nil +} + +func (s *Signed) JSONMarshal(prefix, indent string) ([]byte, error) { + // Marshals Signed with prefix and indent. + b, err := cjson.Marshal(s) + if err != nil { + return []byte{}, err + } + + var out bytes.Buffer + if err := json.Indent(&out, b, prefix, indent); err != nil { + return []byte{}, err + } + + return out.Bytes(), nil +} diff --git a/pkg/cosign/tuf/policy_test.go b/pkg/cosign/tuf/policy_test.go new file mode 100644 index 00000000000..147d9af24ec --- /dev/null +++ b/pkg/cosign/tuf/policy_test.go @@ -0,0 +1,71 @@ +// +// 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. + +// Contains root policy definitions. +// Eventually, this will move this to go-tuf definitions. + +package tuf + +import ( + "encoding/json" + "testing" +) + +func TestAddKey(t *testing.T) { + root := NewRoot() + publicKey := FulcioVerificationKey("test@rekor.dev", "") + if !root.AddKey(publicKey) { + t.Errorf("Adding new key failed") + } + if _, ok := root.Keys[publicKey.ID()]; !ok { + t.Errorf("Error adding public key") + } + // Add duplicate key. + if root.AddKey(publicKey) { + t.Errorf("Duplicate key should not add to dictionary") + } + if len(root.Keys) != 1 { + t.Errorf("Root keys should contain exactly one key.") + } +} + +func TestRootRole(t *testing.T) { + root := NewRoot() + publicKey := FulcioVerificationKey("test@rekor.dev", "") + role := &Role{KeyIDs: []string{}, Threshold: 1} + role.AddKeysWithThreshold([]*Key{publicKey}, 2) + root.Roles["root"] = role + policy, err := root.Marshal() + if err != nil { + t.Errorf("Error marshalling root policy") + } + newRoot := Root{} + if err := json.Unmarshal(policy.Signed, &newRoot); err != nil { + t.Errorf("Error marshalling root policy") + } + rootRole, ok := newRoot.Roles["root"] + if !ok { + t.Errorf("Missing root role") + } + if len(rootRole.KeyIDs) != 1 { + t.Errorf("Missing root key ID") + } + if rootRole.KeyIDs[0] != publicKey.ID() { + t.Errorf("Bad root role key ID") + } + if rootRole.Threshold != 2 { + t.Errorf("Threshold incorrect") + } +} diff --git a/pkg/cosign/tuf/signer.go b/pkg/cosign/tuf/signer.go new file mode 100644 index 00000000000..17ddeeaa9f1 --- /dev/null +++ b/pkg/cosign/tuf/signer.go @@ -0,0 +1,44 @@ +// +// 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 tuf + +import ( + "encoding/json" +) + +const ( + KeyTypeFulcio = "sigstore-oidc" + KeySchemeFulcio = "https://fulcio.sigstore.dev" +) + +var ( + KeyAlgorithms = []string{"sha256", "sha512"} +) + +type FulcioKeyVal struct { + Identity string `json:"identity"` + Issuer string `json:"issuer"` +} + +func FulcioVerificationKey(email string, issuer string) *Key { + keyValBytes, _ := json.Marshal(FulcioKeyVal{Identity: email, Issuer: issuer}) + return &Key{ + Type: KeyTypeFulcio, + Scheme: KeySchemeFulcio, + Algorithms: KeyAlgorithms, + Value: keyValBytes, + } +}