diff --git a/cmd/tuf-notary/delegate.go b/cmd/tuf-notary/delegate.go new file mode 100644 index 0000000..c56d1af --- /dev/null +++ b/cmd/tuf-notary/delegate.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "io/ioutil" + "strings" + + docopt "github.com/docopt/docopt-go" + tufnotary "github.com/notaryproject/tuf/tuf-notary" +) + +func init() { + register("delegate", cmdDelegate, ` +usage: tuf-notary delegate [--repo= --keyfiles= --threshold= --no-passphrase] + +Add a delegation from the top-level targets role to delegatee and +push the updated targets metadata to the TUF reposistory on the registry. + +Options: + --repo Set the tuf repository name. By default this will be 'tuf-repo' + --keyfiles Comma separaged names of public key files stored in tuf-repo/keys that will be used to sign this delegated role. If none are supplied, a keypair will be generated and written to tuf-repo/keys/ + --threshold The threshold for the delegation. By default this will be 1. + `) +} + +func cmdDelegate(args []string, opts docopt.Opts) error { + repository := "tuf-repo" + if r := opts["--repo"]; r != nil { + repository = r.(string) + } + + threshold := 1 + if t := opts["-threshold"]; t != nil { + threshold = t.(int) + } + + keyfiles := []string{} + if k := opts["--keyfiles"]; k != nil { + ks := k.(string) + splitKeys := strings.Split(ks, ",") + for _, key := range splitKeys { + keyfiles = append(keyfiles, key) + } + } + + passphrase := true + if p := opts["--no-passphrase"]; p != nil { + passphrase = !p.(bool) + } + + registry := args[0] + delegatee := args[1] + + err := tufnotary.DownloadTUFMetadata(registry, repository, "root") + if err != nil { + return err + } + err = tufnotary.DownloadTUFMetadata(registry, repository, "targets") + if err != nil { + return err + } + + //add delegation + err = tufnotary.Delegate(repository, delegatee, keyfiles, threshold, passphrase) + + if err != nil { + return err + } + fmt.Println("added delegation to " + delegatee) + + //upload targets with a reference to root metadata + filename := fmt.Sprintf("%s/staged/%s.json", repository, "targets") + contents, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read %s: %w", filename, err) + } + targets_desc, err := tufnotary.UploadTUFMetadata(registry, repository, "targets", contents, "root") + if err != nil { + return err + } + fmt.Println("uploaded targets " + targets_desc.Digest.String()) + + return err +} diff --git a/cmd/tuf-notary/init.go b/cmd/tuf-notary/init.go index 92a09e4..21eafaf 100644 --- a/cmd/tuf-notary/init.go +++ b/cmd/tuf-notary/init.go @@ -47,7 +47,7 @@ func cmdInit(args []string, opts docopt.Opts) error { fmt.Println("uploaded root " + root_desc.Digest.String()) //upload targets with a reference to root metadata - filename = fmt.Sprintf("%s/staged/%s.json", repository, "root") + filename = fmt.Sprintf("%s/staged/%s.json", repository, "targets") contents, err = ioutil.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read %s: %w", filename, err) diff --git a/cmd/tuf-notary/main.go b/cmd/tuf-notary/main.go index 8ef9c94..f0615e2 100644 --- a/cmd/tuf-notary/main.go +++ b/cmd/tuf-notary/main.go @@ -11,11 +11,12 @@ func main() { usage := ` Usage: tuf-notary [....] - tuf-notary [....] [--repo=] + tuf-notary [....] [--repo= --keyfiles= --threshold= --no-passphrase] Commands: help Show usage for a specific command init Initialize a TUF repository + delegate Delegate to a repository from the TUF repository ` args, _ := docopt.ParseDoc(usage) diff --git a/go.mod b/go.mod index 14d0f54..489e910 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect golang.org/x/text v0.3.5 // indirect google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect google.golang.org/grpc v1.38.0 // indirect diff --git a/go.sum b/go.sum index b852afe..6711f05 100644 --- a/go.sum +++ b/go.sum @@ -966,6 +966,7 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/registry-access.go b/registry-access.go index 82bee66..7974823 100644 --- a/registry-access.go +++ b/registry-access.go @@ -10,7 +10,7 @@ import ( func UploadTUFMetadata(registry string, repository string, name string, contents []byte, reference string) (ocispec.Descriptor, error) { ref := registry + "/" + repository + ":" + name - fileName := repository + "/staged/" + name + ".json" + fileName := repository + "/repository/" + name + ".json" mediaType := "application/vnd.cncf.notary.tuf+json" @@ -47,3 +47,21 @@ func UploadTUFMetadata(registry string, repository string, name string, contents return desc, nil } + +func DownloadTUFMetadata(registry string, repository string, name string) error { + ref := registry + "/" + repository + ":" + name + + mediaType := "application/vnd.cncf.notary.tuf+json" + ctx := context.Background() + + reg, err := content.NewRegistry(content.RegistryOptions{PlainHTTP: true}) + if err != nil { + return err + } + + fileStore := content.NewFile("") + defer fileStore.Close() + allowedMediaTypes := []string{mediaType} + _, err = oras.Copy(ctx, reg, ref, fileStore, "", oras.WithAllowedMediaTypes(allowedMediaTypes)) + return err +} diff --git a/tuf-repository.go b/tuf-repository.go index 4243fc6..69917ca 100644 --- a/tuf-repository.go +++ b/tuf-repository.go @@ -1,8 +1,19 @@ package tufnotary import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + "github.com/theupdateframework/go-tuf" + "github.com/theupdateframework/go-tuf/data" + "github.com/theupdateframework/go-tuf/pkg/keys" util "github.com/theupdateframework/go-tuf/util" + "golang.org/x/term" ) func Init(repository string) error { @@ -58,3 +69,132 @@ func Init(repository string) error { err = repo.Timestamp() return err } + +func Delegate(repository string, delegatee string, keyfiles []string, threshold int, passphrase bool) error { + workingDir, err := os.Getwd() + if err != nil { + return err + } + + dir := filepath.Join(workingDir, repository) + + var p util.PassphraseFunc + if passphrase { + p = getPassphrase + } + + repo, err := tuf.NewRepo(tuf.FileSystemStore(dir, p)) + if err != nil { + return err + } + + pubkeys := []*data.PublicKey{} + privkeys := []keys.Signer{} + keyids := []string{} + // if no keyfiles are provided, generate one + if len(keyfiles) < 1 { + key, err := keys.GenerateEd25519Key() + if err != nil { + return err + } + pubkeys = append(pubkeys, key.PublicData()) + privkeys = append(privkeys, key) + fmt.Println(key.PublicData()) + for _, id := range key.PublicData().IDs() { + keyids = append(keyids, id) + } + } else { + for _, filename := range keyfiles { + filePubKeys, err := repo.GetPublicKeys(filename) + if err != nil { + return err + } + for _, filePubKey := range filePubKeys { + pubkeys = append(pubkeys, filePubKey) + for _, keyid := range filePubKey.IDs() { + keyids = append(keyids, keyid) + } + } + } + } + + paths := []string{} + paths = append(paths, delegatee+"/*") + + delegatedRole := data.DelegatedRole{ + Name: delegatee, + KeyIDs: keyids, + Paths: paths, + Threshold: threshold, + } + + err = repo.AddTargetsDelegation("targets", delegatedRole, pubkeys) + if err != nil { + return err + } + + err = repo.Sign(delegatee) + if err != nil { + return err + } + + //if keys were generated, store them + // for k := range privkeys { + // repo.local.SaveSigner(delegatee, k) + // } + + err = repo.Snapshot() + if err != nil { + return err + } + + err = repo.Timestamp() + if err != nil { + return err + } + + err = repo.Commit() + return err +} + +//from go-tuf/cmd/tuf/main.go + +func getPassphrase(role string, confirm bool, change bool) ([]byte, error) { + // In case of change we need to prompt explicitly for a new passphrase + // and not read it from the environment variable, if present + if pass := os.Getenv(fmt.Sprintf("TUF_%s_PASSPHRASE", strings.ToUpper(role))); pass != "" && !change { + return []byte(pass), nil + } + // Alter role string if we are prompting for a passphrase change + if change { + // Check if environment variable for new passphrase exist + if new_pass := os.Getenv(fmt.Sprintf("TUF_NEW_%s_PASSPHRASE", strings.ToUpper(role))); new_pass != "" { + // If so, just read the new passphrase from it and return + return []byte(new_pass), nil + } + // No environment variable set, so proceed prompting for new passphrase + role = fmt.Sprintf("new %s", role) + } + fmt.Printf("Enter %s keys passphrase: ", role) + passphrase, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + if err != nil { + return nil, err + } + + if !confirm { + return passphrase, nil + } + + fmt.Printf("Repeat %s keys passphrase: ", role) + confirmation, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + if err != nil { + return nil, err + } + + if !bytes.Equal(passphrase, confirmation) { + return nil, errors.New("the entered passphrases do not match") + } + return passphrase, nil +}