Skip to content

Commit

Permalink
EXTREME WIP
Browse files Browse the repository at this point in the history
Signed-off-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
  • Loading branch information
trishankatdatadog committed Jun 9, 2020
1 parent afba301 commit 04028e5
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 56 deletions.
58 changes: 58 additions & 0 deletions pkg/tuf/delegations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package tuf

import (
"fmt"
"strings"

"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
"github.com/theupdateframework/notary/tuf/utils"
)

// delegationAdd creates a new delegation by adding a public key from a certificate to a specific role in a GUN
// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go
func delegateToReleases(repo client.Repository, releasesKeyID string) error {
role := data.RoleName("targets/releases")
// How Notary v1 denotes "*""
// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go#L367
allPaths := []string{""}

pubKeys, err := ingestPublicKeys(args)
if err != nil {
return err
}

// Add the delegation to the repository
err = repo.AddDelegation(role, pubKeys, allPaths)
if err != nil {
return fmt.Errorf("failed to create delegation: %v", err)
}

// Make keyID slice for better CLI print
pubKeyIDs := []string{}
for _, pubKey := range pubKeys {
pubKeyID, err := utils.CanonicalKeyID(pubKey)
if err != nil {
return err
}
pubKeyIDs = append(pubKeyIDs, pubKeyID)
}

fmt.Println("")
addingItems := ""
if len(pubKeyIDs) > 0 {
addingItems = addingItems + fmt.Sprintf("with keys %s, ", pubKeyIDs)
}
if d.paths != nil || d.allPaths {
addingItems = addingItems + fmt.Sprintf(
"with paths [%s], ",
strings.Join(prettyPaths(d.paths), "\n"),
)
}
fmt.Printf(
"Addition of delegation role %s %sto repository \"%s\" staged for next publish.\n",
role, addingItems, gun)
fmt.Println("")

return maybeAutoPublish(cmd, d.autoPublish, gun, config, d.retriever)
}
80 changes: 53 additions & 27 deletions pkg/tuf/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func getPassphraseRetriever() notary.PassRetriever {

// Attempt to read a role key from a file, and return it as a data.PrivateKey
// If key is for the Root role, it must be encrypted
func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) {
func readPrivateKey(role data.RoleName, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) {
pemBytes, err := ioutil.ReadFile(keyFilename)
if err != nil {
return nil, fmt.Errorf("Error reading input root key file: %v", err)
Expand All @@ -64,14 +64,40 @@ func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetrie
return privKey, nil
}

// Attempt to read a role key from a file, and return it as a data.PrivateKey
func readPublicKey(args []string) ([]data.PublicKey, error) {
pubKeys := []data.PublicKey{}
if len(args) > 2 {
pubKeyPaths := args[2:]
for _, pubKeyPath := range pubKeyPaths {
// Read public key bytes from PEM file
pubKeyBytes, err := ioutil.ReadFile(pubKeyPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("file for public key does not exist: %s", pubKeyPath)
}
return nil, fmt.Errorf("unable to read public key from file: %s", pubKeyPath)
}

// Parse PEM bytes into type PublicKey
pubKey, err := utils.ParsePEMPublicKey(pubKeyBytes)
if err != nil {
return nil, fmt.Errorf("unable to parse valid public key certificate from PEM file %s: %v", pubKeyPath, err)
}
pubKeys = append(pubKeys, pubKey)
}
}
return pubKeys, nil
}

// importRootKey imports the root key from path then adds the key to repo
// returns key ids
// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/tuf.go#L413
func importRootKey(rootKey string, nRepo client.Repository, retriever notary.PassRetriever) ([]string, error) {
var rootKeyList []string

if rootKey != "" {
privKey, err := readKey(data.CanonicalRootRole, rootKey, retriever)
privKey, err := readPrivateKey(data.CanonicalRootRole, rootKey, retriever)
if err != nil {
return nil, err
}
Expand All @@ -97,25 +123,25 @@ func importRootKey(rootKey string, nRepo client.Repository, retriever notary.Pas
return []string{}, nil
}

// Try to reuse a single targets key across repositories.
// Try to reuse a single key for the given rolename across repositories.
// FIXME: Unfortunately, short of forking Notary or sending a PR upstream, there isn't an easy way to prevent it
// from automagically creating a new, local targets key per TUF metadata repository. We fix this here by undoing
// more than one new, local targets key, and reusing any existing local targets key, just like the way Notary
// reuses the root key.
func reuseTargetsKey(r client.Repository) error {
func reuseKey(r client.Repository, rolename data.RoleName) (string, error) {
var (
err error
thisTargetsKeyID, thatTargetsKeyID string
err error
thisKeyID, thatKeyID string
)

// Get all known targets keys.
targetsKeyList := r.GetCryptoService().ListKeys(data.CanonicalTargetsRole)
// Try to extract a single targets key we can reuse.
switch len(targetsKeyList) {
// Get all known keys for this rolename.
keyList := r.GetCryptoService().ListKeys(rolename)
// Try to extract a single key we can reuse.
switch len(keyList) {
case 0:
err = fmt.Errorf("no targets key despite having initialized a repo")
err = fmt.Errorf("no %s key despite having initialized a repo", rolename)
case 1:
log.Debug("Nothing to do, only one targets key available")
log.Debug("Nothing to do, only one %s key available", rolename)
case 2:
// First, we publish current changes to repository in order to list roles.
// FIXME: Find a find better way to list roles w/o publishing changes first.
Expand All @@ -132,34 +158,34 @@ func reuseTargetsKey(r client.Repository) error {
break
}

// Get the current targets key.
// Get the current key for the given rolename.
// NOTE: We do not delete it, in case the user wants to keep it.
for _, roleWithSig := range roleWithSigs {
role := roleWithSig.Role
if role.Name == data.CanonicalTargetsRole {
if role.Name == rolename {
if len(role.KeyIDs) == 1 {
thisTargetsKeyID = role.KeyIDs[0]
log.Debugf("This targets keyid: %s", thisTargetsKeyID)
thisKeyID = role.KeyIDs[0]
log.Debugf("This %s keyid: %s", rolename, thisKeyID)
} else {
return fmt.Errorf("this targets role has more than 1 key")
return thatKeyID, fmt.Errorf("this %s role has more than 1 key", rolename)
}
}
}

// Get and reuse the other targets key.
for _, keyID := range targetsKeyList {
if keyID != thisTargetsKeyID {
thatTargetsKeyID = keyID
// Get and reuse the other key for the given rolename.
for _, keyID := range keyList {
if keyID != thisKeyID {
thatKeyID = keyID
break
}
}
log.Debugf("That targets keyID: %s", thatTargetsKeyID)
log.Debugf("Before rotating targets key from %s to %s", thisTargetsKeyID, thatTargetsKeyID)
err = r.RotateKey(data.CanonicalTargetsRole, false, []string{thatTargetsKeyID})
log.Debugf("After targets key rotation")
log.Debugf("That %s keyID: %s", rolename, thatKeyID)
log.Debugf("Before rotating %s key from %s to %s", rolename, thisKeyID, thatKeyID)
err = r.RotateKey(rolename, false, []string{thatKeyID})
log.Debugf("After %s key rotation", rolename)
default:
err = fmt.Errorf("there are more than 2 targets keys")
err = fmt.Errorf("there are more than 2 %s keys", rolename)
}

return err
return thatKeyID, err
}
77 changes: 48 additions & 29 deletions pkg/tuf/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,6 @@ import (
"github.com/theupdateframework/notary/tuf/data"
)

// clearChangelist clears the notary staging changelist
func clearChangeList(notaryRepo client.Repository) error {
cl, err := notaryRepo.GetChangelist()
if err != nil {
return err
}
return cl.Clear("")
}

// SignAndPublish signs an artifact, then publishes the metadata to a trust server
func SignAndPublish(trustDir, trustServer, ref, file, tlscacert, rootKey, timeout string, custom *canonicaljson.RawMessage) (*client.Target, error) {
if err := EnsureTrustDir(trustDir); err != nil {
Expand Down Expand Up @@ -50,48 +41,76 @@ func SignAndPublish(trustDir, trustServer, ref, file, tlscacert, rootKey, timeou
if err != nil {
return nil, fmt.Errorf("cannot clear change list: %v", err)
}

defer clearChangeList(repo)

if _, err = repo.ListTargets(); err != nil {
err = reuseKeys(repo, rootKey)
if err != nil {
return nil, fmt.Errorf("cannot reuse keys: %v", err)
}

target, err := client.NewTarget(tag, file, custom)
if err != nil {
return nil, err
}

// If roles is empty, we default to adding to targets
if err = repo.AddTarget(target, data.NewRoleList([]string{})...); err != nil {
return nil, err
}

err = repo.Publish()
return target, err
}

// reuse root and top-level targets keys
func reuseKeys(repo client.Repository, rootKey string) error {
if _, err := repo.ListTargets(); err != nil {
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
// Reuse root key.
rootKeyIDs, err := importRootKey(rootKey, repo, getPassphraseRetriever())
if err != nil {
return nil, err
return err
}

// NOTE: 2nd variadic argument is to indicate that snapshot is managed remotely.
// The impact of a timestamp + snapshot key compromise is not terrible:
// https://docs.docker.com/notary/service_architecture/#threat-model
if err = repo.Initialize(rootKeyIDs, data.CanonicalSnapshotRole); err != nil {
return nil, fmt.Errorf("cannot initialize repo: %v", err)
return fmt.Errorf("cannot initialize repo: %v", err)
}

// Reuse targets key.
if err = reuseTargetsKey(repo); err != nil {
return nil, fmt.Errorf("cannot reuse targets keys: %v", err)
if _, err := reuseKey(repo, data.CanonicalTargetsRole); err != nil {
return fmt.Errorf("cannot reuse %s keys: %v", data.CanonicalTargetsRole)
}

// Reuse targets/releases key.
releasesRoleName := data.RoleName("targets/releases")
// FIXME: logic is faulty right now, because targets/releases will not exist by default.
releasesKeyID, err := reuseKey(repo, releasesRoleName)
if err != nil {
return fmt.Errorf("cannot reuse %s keys: %v", releasesRoleName, err)
}

// Delegate to targets/releases.
err = delegateToReleases(repo, releasesKeyID)
if err != nil {
return fmt.Errorf("cannot delegate to %s: %v", releasesRoleName, err)
}

default:
return nil, fmt.Errorf("cannot list targets: %v", err)
return fmt.Errorf("cannot list targets: %v", err)
}
}
return nil
}

target, err := client.NewTarget(tag, file, custom)
// clearChangelist clears the notary staging changelist
func clearChangeList(notaryRepo client.Repository) error {
cl, err := notaryRepo.GetChangelist()
if err != nil {
return nil, err
}

// TODO - Radu M
// decide whether to allow actually passing roles as flags

// If roles is empty, we default to adding to targets
if err = repo.AddTarget(target, data.NewRoleList([]string{})...); err != nil {
return nil, err
return err
}

err = repo.Publish()
return target, err
return cl.Clear("")
}

0 comments on commit 04028e5

Please sign in to comment.