func TestEncryption(t *testing.T) { s := NewTestImportStore() privKey, err := utils.GenerateECDSAKey(rand.Reader) originalKey := privKey.Private() require.NoError(t, err) pemBytes, err := utils.EncryptPrivateKey(privKey, "", "", "") require.NoError(t, err) in := bytes.NewBuffer(pemBytes) _ = ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.Len(t, s.data, 1) shouldBeEnc, ok := s.data[privKey.ID()] // we should have got a key imported to this location require.True(t, ok) // we should fail to parse it without the passphrase privKey, err = utils.ParsePEMPrivateKey(shouldBeEnc, "") require.Equal(t, err, errors.New("could not decrypt private key")) require.Nil(t, privKey) // we should succeed to parse it with the passphrase privKey, err = utils.ParsePEMPrivateKey(shouldBeEnc, cannedPassphrase) require.NoError(t, err) require.Equal(t, originalKey, privKey.Private()) }
// path and encrypted key should succeed, tests gun inference from path as well func TestEncryptedKeyImportSuccess(t *testing.T) { s := NewTestImportStore() privKey, err := utils.GenerateECDSAKey(rand.Reader) originalKey := privKey.Private() require.NoError(t, err) pemBytes, err := utils.EncryptPrivateKey(privKey, data.CanonicalSnapshotRole, "somegun", cannedPassphrase) require.NoError(t, err) b, _ := pem.Decode(pemBytes) b.Headers["path"] = privKey.ID() pemBytes = pem.EncodeToMemory(b) in := bytes.NewBuffer(pemBytes) _ = ImportKeys(in, []Importer{s}, "", "", passphraseRetriever) require.Len(t, s.data, 1) keyBytes := s.data[privKey.ID()] bFinal, bRest := pem.Decode(keyBytes) require.Equal(t, "somegun", bFinal.Headers["gun"]) require.Len(t, bRest, 0) // we should fail to parse it without the passphrase privKey, err = utils.ParsePEMPrivateKey(keyBytes, "") require.Equal(t, err, errors.New("could not decrypt private key")) require.Nil(t, privKey) // we should succeed to parse it with the passphrase privKey, err = utils.ParsePEMPrivateKey(keyBytes, cannedPassphrase) require.NoError(t, err) require.Equal(t, originalKey, privKey.Private()) }
// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes. // Returns the password and private key func GetPasswdDecryptBytes(passphraseRetriever notary.PassRetriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) { var ( passwd string privKey data.PrivateKey ) for attempts := 0; ; attempts++ { var ( giveup bool err error ) if attempts > 10 { return nil, "", ErrAttemptsExceeded{} } passwd, giveup, err = passphraseRetriever(name, alias, false, attempts) // Check if the passphrase retriever got an error or if it is telling us to give up if giveup || err != nil { return nil, "", ErrPasswordInvalid{} } // Try to convert PEM encoded bytes back to a PrivateKey using the passphrase privKey, err = utils.ParsePEMPrivateKey(pemBytes, passwd) if err == nil { // We managed to parse the PrivateKey. We've succeeded! break } } return privKey, passwd, nil }
// GetKey returns the PrivateKey given a KeyID func (s *GenericKeyStore) GetKey(keyID string) (data.PrivateKey, string, error) { s.Lock() defer s.Unlock() cachedKeyEntry, ok := s.cachedKeys[keyID] if ok { return cachedKeyEntry.key, cachedKeyEntry.alias, nil } role, err := getKeyRole(s.store, keyID) if err != nil { return nil, "", err } keyBytes, err := s.store.Get(keyID) if err != nil { return nil, "", err } // See if the key is encrypted. If its encrypted we'll fail to parse the private key privKey, err := utils.ParsePEMPrivateKey(keyBytes, "") if err != nil { privKey, _, err = GetPasswdDecryptBytes(s.PassRetriever, keyBytes, keyID, string(role)) if err != nil { return nil, "", err } } s.cachedKeys[keyID] = &cachedKey{alias: role, key: privKey} return privKey, role, nil }
// Set determines if we are allowed to set the given key on the Yubikey and // calls through to YubiStore.AddKey if it's valid func (s *YubiImport) Set(name string, bytes []byte) error { block, _ := pem.Decode(bytes) if block == nil { return errors.New("invalid PEM data, could not parse") } role, ok := block.Headers["role"] if !ok { return errors.New("no role found for key") } ki := trustmanager.KeyInfo{ // GUN is ignored by YubiStore Role: role, } privKey, err := utils.ParsePEMPrivateKey(bytes, "") if err != nil { privKey, _, err = trustmanager.GetPasswdDecryptBytes( s.passRetriever, bytes, name, ki.Role, ) if err != nil { return err } } return s.dest.AddKey(ki, privKey) }
// checkValidity ensures the fields in the pem headers are valid and parses out the location. // While importing a collection of keys, errors from this function should result in only the // current pem block being skipped. func checkValidity(block *pem.Block) (string, error) { // 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 switch block.Headers["role"] { case tufdata.CanonicalSnapshotRole, tufdata.CanonicalTargetsRole, tufdata.CanonicalTimestampRole: // check if the key is missing a gun header or has an empty gun and error out since we don't know what gun it belongs to if block.Headers["gun"] == "" { logrus.Infof("failed to import key (%s) to store: Cannot have canonical role key without a gun, don't know what gun it belongs to", block.Headers["path"]) return "", errors.New("invalid key pem block") } default: delete(block.Headers, "gun") } loc, ok := block.Headers["path"] // only if the path isn't specified do we get into this parsing path logic if !ok || loc == "" { // 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") return "", errors.New("invalid key pem block") } loc = decodedKey.ID() } return loc, nil }
// ImportKeys expects an io.Reader containing one or more PEM blocks. // It reads PEM blocks one at a time until pem.Decode returns a nil // block. // 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, fallbackRole string, fallbackGUN string, passRet notary.PassRetriever) error { // importLogic.md contains a small flowchart I made to clear up my understand while writing the cases in this function // it is very rough, but it may help while reading this piece of code data, err := ioutil.ReadAll(from) if err != nil { return err } var ( writeTo string toWrite []byte ) for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) { handleLegacyPath(block) setFallbacks(block, fallbackGUN, fallbackRole) loc, err := checkValidity(block) if err != nil { // already logged in checkValidity 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, // clear toWrite and set writeTo to the next path we're going to write if toWrite != nil { if err = importToStores(to, writeTo, toWrite); err != nil { return err } } // set up for aggregating next file's data toWrite = nil writeTo = loc } toWrite = append(toWrite, blockBytes...) } if toWrite != nil { // close out final iteration if there's data left return importToStores(to, writeTo, toWrite) } return nil }