Skip to content

Commit

Permalink
delegate all paths to targets/releases
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 10, 2020
1 parent afba301 commit ed58458
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 63 deletions.
4 changes: 3 additions & 1 deletion pkg/tuf/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (

"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
"github.com/theupdateframework/notary/tuf/data"
)

const (
dockerConfigDir = ".docker"
dockerConfigDir = ".docker"
releasesRoleName = data.RoleName("targets/releases")
)

func DefaultTrustDir() string {
Expand Down
25 changes: 25 additions & 0 deletions pkg/tuf/delegations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tuf

import (
"fmt"

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

// Delegate all paths ("*") to targets/releases.
// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go
func delegateToReleases(repo client.Repository, publicKey data.PublicKey) error {
// How Notary v1 denotes "*""
// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go#L367
allPaths := []string{""}
publicKeys := []data.PublicKey{publicKey}

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

return nil
}
84 changes: 50 additions & 34 deletions pkg/tuf/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import (
func getPassphraseRetriever() notary.PassRetriever {
baseRetriever := passphrase.PromptRetriever()
env := map[string]string{
"root": os.Getenv("SIGNY_ROOT_PASSPHRASE"),
"targets": os.Getenv("SIGNY_TARGETS_PASSPHRASE"),
"releases": os.Getenv("SIGNY_RELEASES_PASSPHRASE"),
"root": os.Getenv("SIGNY_ROOT_PASSPHRASE"),
"targets": os.Getenv("SIGNY_TARGETS_PASSPHRASE"),
"targets/releases": os.Getenv("SIGNY_RELEASES_PASSPHRASE"),
}

return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
Expand All @@ -39,18 +39,20 @@ 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)
}

isEncrypted := true
if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil {
if role == data.CanonicalRootRole {
return nil, err
}
isEncrypted = false
}

var privKey data.PrivateKey
if isEncrypted {
privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole.String())
Expand All @@ -71,7 +73,7 @@ func importRootKey(rootKey string, nRepo client.Repository, retriever notary.Pas
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 @@ -89,77 +91,91 @@ func importRootKey(rootKey string, nRepo client.Repository, retriever notary.Pas
// Chooses the first root key available, which is initialization specific
// but should return the HW one first.
rootKeyID := rootKeyList[0]
log.Debugf("Signy found root key, using: %s\n", rootKeyID)

log.Debugf("found root key: %s\n", rootKeyID)
return []string{rootKeyID}, nil
}

return []string{}, nil
}

// Try to reuse a single targets/releases key across repositories.
func reuseReleasesKey(r client.Repository) (data.PublicKey, error) {
// Get all known targets keys.
cryptoService := r.GetCryptoService()
keyList := cryptoService.ListKeys(releasesRoleName)

// Try to extract a single targets/releases key we can reuse.
switch len(keyList) {
case 0:
log.Debug("No %s key available, need to make one", releasesRoleName)
return cryptoService.Create(releasesRoleName, r.GetGUN(), data.ECDSAKey)
case 1:
log.Debug("Nothing to do, only one %s key available", releasesRoleName)
return cryptoService.GetKey(keyList[0]), nil
default:
return nil, fmt.Errorf("there is more than one %s keys", releasesRoleName)
}
}

// Try to reuse a single targets key 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 {
var (
err error
thisTargetsKeyID, thatTargetsKeyID string
)

// Get all known targets keys.
targetsKeyList := r.GetCryptoService().ListKeys(data.CanonicalTargetsRole)
keyList := r.GetCryptoService().ListKeys(data.CanonicalTargetsRole)

// Try to extract a single targets key we can reuse.
switch len(targetsKeyList) {
switch len(keyList) {
case 0:
err = fmt.Errorf("no targets key despite having initialized a repo")
return fmt.Errorf("no targets key despite having initialized a repo")
case 1:
log.Debug("Nothing to do, only one targets key available")
return nil
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.
publishErr := r.Publish()
if publishErr != nil {
err = publishErr
break
err := r.Publish()
if err != nil {
return err
}

// Get the current top-level roles.
roleWithSigs, listRolesErr := r.ListRoles()
if listRolesErr != nil {
err = listRolesErr
break
roleWithSigs, err := r.ListRoles()
if err != nil {
return err
}

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

// Get and reuse the other targets key.
for _, keyID := range targetsKeyList {
if keyID != thisTargetsKeyID {
thatTargetsKeyID = keyID
var thatKeyID string
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("That targets keyID: %s", thatKeyID)
log.Debugf("Before rotating targets key from %s to %s", thisKeyID, thatKeyID)
err = r.RotateKey(data.CanonicalTargetsRole, false, []string{thatKeyID})
log.Debugf("After targets key rotation")
return err
default:
err = fmt.Errorf("there are more than 2 targets keys")
return fmt.Errorf("there are more than two targets keys")
}

return err
}
73 changes: 45 additions & 28 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,74 @@ 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)
return fmt.Errorf("cannot reuse %s keys: %v", data.CanonicalTargetsRole)
}

// Reuse targets/releases key.
releasesPublicKey, err := reuseReleasesKey(repo)
if err != nil {
return fmt.Errorf("cannot reuse %s keys: %v", releasesRoleName, err)
}

// Delegate to targets/releases.
err = delegateToReleases(repo, releasesPublicKey)
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 ed58458

Please sign in to comment.