diff --git a/cmd/tuf-notary/main.go b/cmd/tuf-notary/main.go index 8ef9c94..2d28c85 100644 --- a/cmd/tuf-notary/main.go +++ b/cmd/tuf-notary/main.go @@ -16,6 +16,8 @@ Usage: Commands: help Show usage for a specific command init Initialize a TUF repository + snapshot Create and upload snapshot + timestamp Create and upload timestamp ` args, _ := docopt.ParseDoc(usage) diff --git a/cmd/tuf-notary/snapshot.go b/cmd/tuf-notary/snapshot.go new file mode 100644 index 0000000..ce1c514 --- /dev/null +++ b/cmd/tuf-notary/snapshot.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "io/ioutil" + + docopt "github.com/docopt/docopt-go" + tufnotary "github.com/notaryproject/tuf/tuf-notary" +) + +func init() { + register("snapshot", cmdSnapshot, ` +usage: tuf-notary snapshot [--repo=] + +Generate snapshot metadata and push it to the TUF repository on the +registry + +Options: + --repo Set the tuf repository name. By default this will be 'tuf-repo' + `) +} + +func cmdSnapshot(args []string, opts docopt.Opts) error { + repository := "tuf-repo" + if r := opts["--repo"]; r != nil { + repository = r.(string) + } + + registry := args[0] + + err := tufnotary.DownloadTUFMetadata(registry, repository, "root") + if err != nil { + return err + } + err = tufnotary.DownloadTUFMetadata(registry, repository, "targets") + if err != nil { + return err + } + + //TODO: ensure that delegated targets are also included + //TODO: get passphrase bool from argument + err = tufnotary.Snapshot(repository, false) + + if err != nil { + return err + } + + //upload snapshot with a reference to root metadata + filename := fmt.Sprintf("%s/staged/%s.json", repository, "snapshot") + contents, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read %s: %w", filename, err) + } + snapshot_desc, err := tufnotary.UploadTUFMetadata(registry, repository, "snapshot", contents, "root") + if err != nil { + return err + } + fmt.Println("uploaded snapshot " + snapshot_desc.Digest.String()) + + return err +} diff --git a/cmd/tuf-notary/timestamp.go b/cmd/tuf-notary/timestamp.go new file mode 100644 index 0000000..9bc1e35 --- /dev/null +++ b/cmd/tuf-notary/timestamp.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "io/ioutil" + + docopt "github.com/docopt/docopt-go" + tufnotary "github.com/notaryproject/tuf/tuf-notary" +) + +func init() { + register("timestamp", cmdTimestamp, ` +usage: tuf-notary timestamp [--repo=] + +Generate timestamp metadata and push it to the TUF repository on the +registry + +Options: + --repo Set the tuf repository name. By default this will be 'tuf-repo' + `) +} + +func cmdTimestamp(args []string, opts docopt.Opts) error { + repository := "tuf-repo" + if r := opts["--repo"]; r != nil { + repository = r.(string) + } + + registry := args[0] + + err := tufnotary.DownloadTUFMetadata(registry, repository, "root") + if err != nil { + return err + } + err = tufnotary.DownloadTUFMetadata(registry, repository, "targets") + if err != nil { + return err + } + //TODO: verify the snapshot before adding timestamp + err = tufnotary.DownloadTUFMetadata(registry, repository, "snapshot") + if err != nil { + return err + } + + //TODO: get passphrase bool from argument + err = tufnotary.Timestamp(repository, false) + + if err != nil { + return err + } + + //upload timestamp with a reference to root metadata + filename := fmt.Sprintf("%s/staged/%s.json", repository, "timestamp") + contents, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read %s: %w", filename, err) + } + timestamp_desc, err := tufnotary.UploadTUFMetadata(registry, repository, "timestamp", contents, "root") + if err != nil { + return err + } + fmt.Println("uploaded timestamp " + timestamp_desc.Digest.String()) + + return err +} 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..bb9f10b 100644 --- a/registry-access.go +++ b/registry-access.go @@ -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..18955b4 100644 --- a/tuf-repository.go +++ b/tuf-repository.go @@ -1,8 +1,17 @@ package tufnotary import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "syscall" + "github.com/theupdateframework/go-tuf" util "github.com/theupdateframework/go-tuf/util" + "golang.org/x/crypto/ssh/terminal" ) func Init(repository string) error { @@ -58,3 +67,79 @@ func Init(repository string) error { err = repo.Timestamp() return err } + +func Snapshot(repository string, 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 + } + + repo.Snapshot() + repo.Commit() + return nil +} + +func Timestamp(repository string, 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 + } + + repo.Timestamp() + repo.Commit() + return nil +} + +//from go-tuf/cmd/tuf/main.go +func getPassphrase(role string, confirm bool) ([]byte, error) { + if pass := os.Getenv(fmt.Sprintf("TUF_%s_PASSPHRASE", strings.ToUpper(role))); pass != "" { + return []byte(pass), nil + } + + fmt.Printf("Enter %s keys passphrase: ", role) + passphrase, err := terminal.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 := terminal.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 +}