Skip to content

Commit

Permalink
Add a policy-init using TUF metadata and Fulcio signers (#469)
Browse files Browse the repository at this point in the history
* add policy init with tuf

Signed-off-by: Asra Ali <[email protected]>

* update go-tuf to my local fork for ease

Signed-off-by: Asra Ali <[email protected]>

* clean up

Signed-off-by: Asra Ali <[email protected]>

* add subcommand

Signed-off-by: Asra Ali <[email protected]>
  • Loading branch information
asraa authored Sep 30, 2021
1 parent 5e2ee28 commit a568dad
Show file tree
Hide file tree
Showing 9 changed files with 470 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/cosign/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func New() *cobra.Command {

// Add sub-commands.
addPublicKey(cmd)
addPolicy(cmd)
addGenerate(cmd)
addSign(cmd)
addSignBlob(cmd)
Expand Down
45 changes: 45 additions & 0 deletions cmd/cosign/cli/options/policy.go
Original file line number Diff line number Diff line change
@@ -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")
}
126 changes: 126 additions & 0 deletions cmd/cosign/cli/policy_init.go
Original file line number Diff line number Diff line change
@@ -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 <project_namespace> --maintainers {email_addresses} --threshold <int> --expires <int>(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)
}
33 changes: 33 additions & 0 deletions cmd/cosign/cli/policy_init_test.go
Original file line number Diff line number Diff line change
@@ -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 := "[email protected]"
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)
}
}
2 changes: 1 addition & 1 deletion cmd/cosign/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 148 additions & 0 deletions pkg/cosign/tuf/policy.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit a568dad

Please sign in to comment.