-
Notifications
You must be signed in to change notification settings - Fork 510
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
adding --role flag to key import #882
Changes from all commits
c97ceff
ad9ecf3
7f709c9
66be5d9
1a5b332
13d986a
e19af6b
ba2508f
d8a9ca9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,12 @@ package utils | |
|
||
import ( | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
"github.com/Sirupsen/logrus" | ||
"github.com/docker/notary" | ||
tufdata "github.com/docker/notary/tuf/data" | ||
"github.com/docker/notary/tuf/utils" | ||
"io" | ||
"io/ioutil" | ||
"path/filepath" | ||
|
@@ -26,7 +29,7 @@ type Importer interface { | |
// ExportKeysByGUN exports all keys filtered to a GUN | ||
func ExportKeysByGUN(to io.Writer, s Exporter, gun string) error { | ||
keys := s.ListFiles() | ||
sort.Strings(keys) // ensure consistenct. ListFiles has no order guarantee | ||
sort.Strings(keys) // ensure consistency. ListFiles has no order guarantee | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for fixing this typo! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. of course 😄 |
||
for _, k := range keys { | ||
dir := filepath.Dir(k) | ||
if dir == gun { // must be full GUN match | ||
|
@@ -93,7 +96,7 @@ func ExportKeys(to io.Writer, s Exporter, from string) error { | |
// Each block is written to the subpath indicated in the "path" PEM | ||
// header. If the file already exists, the file is truncated. Multiple | ||
// adjacent PEMs with the same "path" header are appended together. | ||
func ImportKeys(from io.Reader, to []Importer) error { | ||
func ImportKeys(from io.Reader, to []Importer, fallbackRole string, fallbackGun string, passRet notary.PassRetriever) error { | ||
data, err := ioutil.ReadAll(from) | ||
if err != nil { | ||
return err | ||
|
@@ -103,11 +106,91 @@ func ImportKeys(from io.Reader, to []Importer) error { | |
toWrite []byte | ||
) | ||
for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { | ||
// if there is a path then we set the gun header from this path | ||
if rawPath := block.Headers["path"]; rawPath != "" { | ||
pathWOFileName := strings.TrimSuffix(rawPath, filepath.Base(rawPath)) | ||
if strings.HasPrefix(pathWOFileName, notary.NonRootKeysSubdir) { | ||
gunName := strings.TrimPrefix(pathWOFileName, notary.NonRootKeysSubdir) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a test-case that if we initialize a repo, export keys, then import keys to a fresh file-based keystore the Also, for this logic - it seems like delegation keys may end up with a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, thanks 😄 |
||
gunName = gunName[1:(len(gunName) - 1)] // remove the slashes | ||
if gunName != "" { | ||
block.Headers["gun"] = gunName | ||
} | ||
} | ||
} | ||
if block.Headers["gun"] == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking: this has probably already been discussed and resolved, and I just completely forgot all the cases, but is it ok for the gun and path to mismatch? e.g. gun to be This wouldn't affect where to store it at all, just a field which is not currently used but may be used in the future. |
||
if fallbackGun != "" { | ||
block.Headers["gun"] = fallbackGun | ||
} | ||
} | ||
if block.Headers["role"] == "" { | ||
if fallbackRole == "" { | ||
block.Headers["role"] = notary.DefaultImportRole | ||
} else { | ||
block.Headers["role"] = fallbackRole | ||
} | ||
} | ||
loc, ok := block.Headers["path"] | ||
// only if the path isn't specified do we get into this parsing path logic | ||
if !ok || loc == "" { | ||
logrus.Info("failed to import key to store: PEM headers did not contain import path") | ||
continue // don't know where to copy this key. Skip it. | ||
// if the path isn't specified, we will try to infer the path rel to trust dir from the role (and then gun) | ||
// parse key for the keyID which we will save it by. | ||
// if the key is encrypted at this point, we will generate an error and continue since we don't know the ID to save it by | ||
decodedKey, err := utils.ParsePEMPrivateKey(pem.EncodeToMemory(block), "") | ||
if err != nil { | ||
logrus.Info("failed to import key to store: Invalid key generated, key may be encrypted and does not contain path header") | ||
continue | ||
} | ||
keyID := decodedKey.ID() | ||
switch block.Headers["role"] { | ||
case tufdata.CanonicalRootRole: | ||
// this is a root key so import it to trustDir/root_keys/ | ||
loc = filepath.Join(notary.RootKeysSubdir, keyID) | ||
case tufdata.CanonicalSnapshotRole, tufdata.CanonicalTargetsRole, tufdata.CanonicalTimestampRole: | ||
// this is a canonical key | ||
loc = filepath.Join(notary.NonRootKeysSubdir, block.Headers["gun"], keyID) | ||
default: | ||
//this is a delegation key | ||
loc = filepath.Join(notary.NonRootKeysSubdir, keyID) | ||
} | ||
} | ||
|
||
// A root key or a delegations key should not have a gun | ||
// Note that a key that is not any of the canonical roles (except root) is a delegations key and should not have a gun | ||
if block.Headers["role"] != tufdata.CanonicalSnapshotRole && block.Headers["role"] != tufdata.CanonicalTargetsRole && block.Headers["role"] != tufdata.CanonicalTimestampRole { | ||
delete(block.Headers, "gun") | ||
} else { | ||
// check if the key is missing a gun header or has an empty gun and error out since we don't know where to import this key to | ||
if block.Headers["gun"] == "" { | ||
logrus.Info("failed to import key to store: Cannot have canonical role key without a gun, don't know where to import it") | ||
continue | ||
} | ||
} | ||
|
||
// the path header is not of any use once we've imported the key so strip it away | ||
delete(block.Headers, "path") | ||
|
||
// we are now all set for import but let's first encrypt the key | ||
blockBytes := pem.EncodeToMemory(block) | ||
// check if key is encrypted, note: if it is encrypted at this point, it will have had a path header | ||
if privKey, err := utils.ParsePEMPrivateKey(blockBytes, ""); err == nil { | ||
// Key is not encrypted- ask for a passphrase and encrypt this key | ||
var chosenPassphrase string | ||
for attempts := 0; ; attempts++ { | ||
var giveup bool | ||
chosenPassphrase, giveup, err = passRet(loc, block.Headers["role"], true, attempts) | ||
if err == nil { | ||
break | ||
} | ||
if giveup || attempts > 10 { | ||
return errors.New("maximum number of passphrase attempts exceeded") | ||
} | ||
} | ||
blockBytes, err = utils.EncryptPrivateKey(privKey, block.Headers["role"], block.Headers["gun"], chosenPassphrase) | ||
if err != nil { | ||
return errors.New("failed to encrypt key with given passphrase") | ||
} | ||
} | ||
|
||
if loc != writeTo { | ||
// next location is different from previous one. We've finished aggregating | ||
// data for the previous file. If we have data, write the previous file, | ||
|
@@ -121,8 +204,8 @@ func ImportKeys(from io.Reader, to []Importer) error { | |
toWrite = nil | ||
writeTo = loc | ||
} | ||
delete(block.Headers, "path") | ||
toWrite = append(toWrite, pem.EncodeToMemory(block)...) | ||
|
||
toWrite = append(toWrite, blockBytes...) | ||
} | ||
if toWrite != nil { // close out final iteration if there's data left | ||
return importToStores(to, writeTo, toWrite) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we rephrase both of these role and gun flag descriptions to something shorter like:
Role to import key with, if a role is not already given in a PEM header
I think the pathing logic can be explained from informative error messages rather than adding complexity to the CLI flags, since the error should depend on how the user imports the keys (straight from openssl vs. from
notary key export
)