Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for delegating to a repository #25

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions cmd/tuf-notary/delegate.go
Original file line number Diff line number Diff line change
@@ -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 <registry> <delegateeName> [--repo=<repository> --keyfiles=<names> --threshold=<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/<delegate>
--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
}
2 changes: 1 addition & 1 deletion cmd/tuf-notary/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion cmd/tuf-notary/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ func main() {
usage := `
Usage:
tuf-notary <command> [<args>....]
tuf-notary <command> [<args>....] [--repo=<repository>]
tuf-notary <command> [<args>....] [--repo=<repository> --keyfiles=<names> --threshold=<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)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
20 changes: 19 additions & 1 deletion registry-access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
140 changes: 140 additions & 0 deletions tuf-repository.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
}