mirror of https://github.com/docker/docs.git
				
				
				
			Merge pull request #39 from docker/fix-import
Do not back up a root key that is imported into Yubikey. Signed-off-by: David Lawrence <david.lawrence@docker.com> Signed-off-by: Diogo Mónica <diogo.monica@gmail.com> (github: endophage)
This commit is contained in:
		
						commit
						91b7d87a7b
					
				|  | @ -178,17 +178,18 @@ func GetKeys(t *testing.T, tempDir string) ([]string, []string) { | |||
| 
 | ||||
| // List keys, parses the output, and asserts something about the number of root
 | ||||
| // keys and number of signing keys, as well as returning them.
 | ||||
| func assertNumKeys(t *testing.T, tempDir string, numRoot, numSigning int) ( | ||||
| 	[]string, []string) { | ||||
| func assertNumKeys(t *testing.T, tempDir string, numRoot, numSigning int, | ||||
| 	rootOnDisk bool) ([]string, []string) { | ||||
| 
 | ||||
| 	root, signing := GetKeys(t, tempDir) | ||||
| 	assert.Len(t, root, numRoot) | ||||
| 	assert.Len(t, signing, numSigning) | ||||
| 	for _, rootKeyID := range root { | ||||
| 		// it should always be present on disk
 | ||||
| 		_, err := os.Stat(filepath.Join( | ||||
| 			tempDir, "private", "root_keys", rootKeyID+"_root.key")) | ||||
| 		assert.NoError(t, err) | ||||
| 		// os.IsExist checks to see if the error is because a file already
 | ||||
| 		// exist, and hence doesn't actually the right funciton to use here
 | ||||
| 		assert.Equal(t, rootOnDisk, !os.IsNotExist(err)) | ||||
| 
 | ||||
| 		// this function is declared is in the build-tagged setup files
 | ||||
| 		verifyRootKeyOnHardware(t, rootKeyID) | ||||
|  | @ -241,17 +242,17 @@ func TestClientKeyGenerationRotation(t *testing.T) { | |||
| 	// -- tests --
 | ||||
| 
 | ||||
| 	// starts out with no keys
 | ||||
| 	assertNumKeys(t, tempDir, 0, 0) | ||||
| 	assertNumKeys(t, tempDir, 0, 0, true) | ||||
| 
 | ||||
| 	// generate root key produces a single root key and no other keys
 | ||||
| 	_, err = runCommand(t, tempDir, "key", "generate", "ecdsa") | ||||
| 	assert.NoError(t, err) | ||||
| 	assertNumKeys(t, tempDir, 1, 0) | ||||
| 	assertNumKeys(t, tempDir, 1, 0, true) | ||||
| 
 | ||||
| 	// initialize a repo, should have signing keys and no new root key
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	origRoot, origSign := assertNumKeys(t, tempDir, 1, 2) | ||||
| 	origRoot, origSign := assertNumKeys(t, tempDir, 1, 2, true) | ||||
| 
 | ||||
| 	// publish using the original keys
 | ||||
| 	assertSuccessfullyPublish(t, tempDir, server.URL, "gun", target, tempfiles[0]) | ||||
|  | @ -259,7 +260,7 @@ func TestClientKeyGenerationRotation(t *testing.T) { | |||
| 	// rotate the signing keys
 | ||||
| 	_, err = runCommand(t, tempDir, "key", "rotate", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	root, sign := assertNumKeys(t, tempDir, 1, 4) | ||||
| 	root, sign := assertNumKeys(t, tempDir, 1, 4, true) | ||||
| 	assert.Equal(t, origRoot[0], root[0]) | ||||
| 	// there should be the new keys and the old keys
 | ||||
| 	for _, origKey := range origSign { | ||||
|  | @ -275,7 +276,7 @@ func TestClientKeyGenerationRotation(t *testing.T) { | |||
| 	// publish the key rotation
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	root, sign = assertNumKeys(t, tempDir, 1, 2) | ||||
| 	root, sign = assertNumKeys(t, tempDir, 1, 2, true) | ||||
| 	assert.Equal(t, origRoot[0], root[0]) | ||||
| 	// just do a cursory rotation check that the keys aren't equal anymore
 | ||||
| 	for _, origKey := range origSign { | ||||
|  | @ -332,7 +333,7 @@ func TestClientKeyImportExportRootAndSigning(t *testing.T) { | |||
| 		assertSuccessfullyPublish( | ||||
| 			t, dirs[0], server.URL, gun, target, tempfiles[0]) | ||||
| 	} | ||||
| 	assertNumKeys(t, dirs[0], 1, 4) | ||||
| 	assertNumKeys(t, dirs[0], 1, 4, true) | ||||
| 
 | ||||
| 	// -- tests --
 | ||||
| 	zipfile := tempfiles[0] + ".zip" | ||||
|  | @ -344,7 +345,7 @@ func TestClientKeyImportExportRootAndSigning(t *testing.T) { | |||
| 
 | ||||
| 	_, err = runCommand(t, dirs[1], "key", "import", zipfile) | ||||
| 	assert.NoError(t, err) | ||||
| 	assertNumKeys(t, dirs[1], 1, 4) // all keys should be there
 | ||||
| 	assertNumKeys(t, dirs[1], 1, 4, true) // all keys should be there
 | ||||
| 
 | ||||
| 	// can list and publish to both repos using imported keys
 | ||||
| 	for _, gun := range []string{"gun1", "gun2"} { | ||||
|  | @ -365,16 +366,12 @@ func TestClientKeyImportExportRootAndSigning(t *testing.T) { | |||
| 
 | ||||
| 	// this function is declared is in the build-tagged setup files
 | ||||
| 	if rootOnHardware() { | ||||
| 		// hardware root is still present, but two signing keys moved - we
 | ||||
| 		// can't call assertNumKeys, because in this case it will ONLY be on
 | ||||
| 		// hardware and not on disk
 | ||||
| 		root, signing := GetKeys(t, dirs[2]) | ||||
| 		assert.Len(t, root, 1) | ||||
| 		verifyRootKeyOnHardware(t, root[0]) | ||||
| 		assert.Len(t, signing, 2) | ||||
| 		// hardware root is still present, and the key will ONLY be on hardware
 | ||||
| 		// and not on disk
 | ||||
| 		assertNumKeys(t, dirs[2], 1, 2, false) | ||||
| 	} else { | ||||
| 		// only 2 signing keys should be there, and no root key
 | ||||
| 		assertNumKeys(t, dirs[2], 0, 2) | ||||
| 		assertNumKeys(t, dirs[2], 0, 2, true) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -388,7 +385,7 @@ func exportRoot(t *testing.T, exportTo string) string { | |||
| 	// generate root key produces a single root key and no other keys
 | ||||
| 	_, err = runCommand(t, tempDir, "key", "generate", "ecdsa") | ||||
| 	assert.NoError(t, err) | ||||
| 	oldRoot, _ := assertNumKeys(t, tempDir, 1, 0) | ||||
| 	oldRoot, _ := assertNumKeys(t, tempDir, 1, 0, true) | ||||
| 
 | ||||
| 	// export does not require a password
 | ||||
| 	oldRetriever := retriever | ||||
|  | @ -451,13 +448,16 @@ func TestClientKeyImportExportRootOnly(t *testing.T) { | |||
| 	// import the key
 | ||||
| 	_, err = runCommand(t, tempDir, "key", "import-root", tempFile.Name()) | ||||
| 	assert.NoError(t, err) | ||||
| 	newRoot, _ := assertNumKeys(t, tempDir, 1, 0) | ||||
| 
 | ||||
| 	// if there is hardware available, root will only be on hardware, and not
 | ||||
| 	// on disk
 | ||||
| 	newRoot, _ := assertNumKeys(t, tempDir, 1, 0, !rootOnHardware()) | ||||
| 	assert.Equal(t, rootKeyID, newRoot[0]) | ||||
| 
 | ||||
| 	// Just to make sure, init a repo and publish
 | ||||
| 	_, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") | ||||
| 	assert.NoError(t, err) | ||||
| 	assertNumKeys(t, tempDir, 1, 2) | ||||
| 	assertNumKeys(t, tempDir, 1, 2, !rootOnHardware()) | ||||
| 	assertSuccessfullyPublish( | ||||
| 		t, tempDir, server.URL, "gun", target, tempFile.Name()) | ||||
| } | ||||
|  |  | |||
|  | @ -112,8 +112,8 @@ func (cs *CryptoService) ImportRootKey(source io.Reader) error { | |||
| 
 | ||||
| 	for _, ks := range cs.keyStores { | ||||
| 		// don't redeclare err, we want the value carried out of the loop
 | ||||
| 		if err = ks.ImportKey(pemBytes, "root"); err != nil { | ||||
| 			continue | ||||
| 		if err = ks.ImportKey(pemBytes, "root"); err == nil { | ||||
| 			return nil //bail on the first keystore we import to
 | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -124,12 +124,17 @@ func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]s | |||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		withID := fmt.Sprintf(" with ID %s", shortName) | ||||
| 		if shortName == "" { | ||||
| 			withID = "" | ||||
| 		} | ||||
| 
 | ||||
| 		if createNew { | ||||
| 			fmt.Fprintf(out, "Enter passphrase for new %s key with ID %s: ", displayAlias, shortName) | ||||
| 			fmt.Fprintf(out, "Enter passphrase for new %s key%s: ", displayAlias, withID) | ||||
| 		} else if displayAlias == "yubikey" { | ||||
| 			fmt.Fprintf(out, "Enter the %s for the attached Yubikey: ", keyName) | ||||
| 		} else { | ||||
| 			fmt.Fprintf(out, "Enter passphrase for %s key with ID %s: ", displayAlias, shortName) | ||||
| 			fmt.Fprintf(out, "Enter passphrase for %s key%s: ", displayAlias, withID) | ||||
| 		} | ||||
| 
 | ||||
| 		passphrase, err := stdin.ReadBytes('\n') | ||||
|  | @ -157,7 +162,7 @@ func PromptRetrieverWithInOut(in io.Reader, out io.Writer, aliasMap map[string]s | |||
| 			return "", false, ErrTooShort | ||||
| 		} | ||||
| 
 | ||||
| 		fmt.Fprintf(out, "Repeat passphrase for new %s key with ID %s: ", displayAlias, shortName) | ||||
| 		fmt.Fprintf(out, "Repeat passphrase for new %s key%s: ", displayAlias, withID) | ||||
| 		confirmation, err := stdin.ReadBytes('\n') | ||||
| 		fmt.Fprintln(out) | ||||
| 		if err != nil { | ||||
|  |  | |||
|  | @ -347,7 +347,8 @@ func importKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cac | |||
| 		return s.Add(alias, pemBytes) | ||||
| 	} | ||||
| 
 | ||||
| 	privKey, passphrase, err := GetPasswdDecryptBytes(passphraseRetriever, pemBytes, "imported", alias) | ||||
| 	privKey, passphrase, err := GetPasswdDecryptBytes( | ||||
| 		passphraseRetriever, pemBytes, "", "imported "+alias) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
|  |  | |||
|  | @ -199,9 +199,11 @@ func addECDSAKey( | |||
| 		return fmt.Errorf("error importing: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	err = backupStore.AddKey(privKey.ID(), role, privKey) | ||||
| 	if err != nil { | ||||
| 		return ErrBackupFailed{err: err.Error()} | ||||
| 	if backupStore != nil { | ||||
| 		err = backupStore.AddKey(privKey.ID(), role, privKey) | ||||
| 		if err != nil { | ||||
| 			return ErrBackupFailed{err: err.Error()} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
|  | @ -553,6 +555,11 @@ func (s *YubiKeyStore) ListKeys() map[string]string { | |||
| 
 | ||||
| // AddKey puts a key inside the Yubikey, as well as writing it to the backup store
 | ||||
| func (s *YubiKeyStore) AddKey(keyID, role string, privKey data.PrivateKey) error { | ||||
| 	return s.addKey(keyID, role, privKey, true) | ||||
| } | ||||
| 
 | ||||
| func (s *YubiKeyStore) addKey( | ||||
| 	keyID, role string, privKey data.PrivateKey, backup bool) error { | ||||
| 	// We only allow adding root keys for now
 | ||||
| 	if role != data.CanonicalRootRole { | ||||
| 		return fmt.Errorf("yubikey only supports storing root keys, got %s for key: %s\n", role, keyID) | ||||
|  | @ -576,7 +583,14 @@ func (s *YubiKeyStore) AddKey(keyID, role string, privKey data.PrivateKey) error | |||
| 		return err | ||||
| 	} | ||||
| 	logrus.Debugf("Using yubikey slot %v", slot) | ||||
| 	err = addECDSAKey(ctx, session, privKey, slot, s.passRetriever, role, s.backupStore) | ||||
| 
 | ||||
| 	backupStore := s.backupStore | ||||
| 	if !backup { | ||||
| 		backupStore = nil | ||||
| 	} | ||||
| 
 | ||||
| 	err = addECDSAKey( | ||||
| 		ctx, session, privKey, slot, s.passRetriever, role, backupStore) | ||||
| 	if err == nil { | ||||
| 		s.keys[privKey.ID()] = yubiSlot{ | ||||
| 			role:   role, | ||||
|  | @ -636,18 +650,21 @@ func (s *YubiKeyStore) RemoveKey(keyID string) error { | |||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // ExportKey doesn't work, because you can't export data from a Yubikey
 | ||||
| func (s *YubiKeyStore) ExportKey(keyID string) ([]byte, error) { | ||||
| 	logrus.Debugf("Attempting to export: %s key inside of YubiKeyStore", keyID) | ||||
| 	return nil, errors.New("Keys cannot be exported from a Yubikey.") | ||||
| } | ||||
| 
 | ||||
| // ImportKey imports a root key into a Yubikey
 | ||||
| func (s *YubiKeyStore) ImportKey(pemBytes []byte, keyID string) error { | ||||
| 	logrus.Debugf("Attempting to import: %s key inside of YubiKeyStore", keyID) | ||||
| 	privKey, _, err := GetPasswdDecryptBytes(s.passRetriever, pemBytes, "imported", "root") | ||||
| 	privKey, _, err := GetPasswdDecryptBytes( | ||||
| 		s.passRetriever, pemBytes, "", "imported root") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return s.AddKey(privKey.ID(), "root", privKey) | ||||
| 	return s.addKey(privKey.ID(), "root", privKey, false) | ||||
| } | ||||
| 
 | ||||
| func cleanup(ctx *pkcs11.Ctx, session pkcs11.SessionHandle) { | ||||
|  |  | |||
|  | @ -78,3 +78,42 @@ func TestAddKeyToNextEmptyYubikeySlot(t *testing.T) { | |||
| 	_, err = testAddKey(t, store) | ||||
| 	assert.NoError(t, err) | ||||
| } | ||||
| 
 | ||||
| // ImportKey imports a key as root without adding it to the backup store
 | ||||
| func TestImportKey(t *testing.T) { | ||||
| 	if !YubikeyAccessible() { | ||||
| 		t.Skip("Must have Yubikey access.") | ||||
| 	} | ||||
| 	clearAllKeys(t) | ||||
| 
 | ||||
| 	ret := passphrase.ConstantRetriever("passphrase") | ||||
| 	backup := NewKeyMemoryStore(ret) | ||||
| 	store, err := NewYubiKeyStore(backup, ret) | ||||
| 	assert.NoError(t, err) | ||||
| 	SetYubikeyKeyMode(KeymodeNone) | ||||
| 	defer func() { | ||||
| 		SetYubikeyKeyMode(KeymodeTouch | KeymodePinOnce) | ||||
| 	}() | ||||
| 
 | ||||
| 	// generate key and import it
 | ||||
| 	privKey, err := GenerateECDSAKey(rand.Reader) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pemBytes, err := EncryptPrivateKey(privKey, "passphrase") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	err = store.ImportKey(pemBytes, privKey.ID()) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// key is not in backup store
 | ||||
| 	_, _, err = backup.GetKey(privKey.ID()) | ||||
| 	assert.Error(t, err) | ||||
| 
 | ||||
| 	// ensure key is in Yubikey -  create a new store, to make sure we're not
 | ||||
| 	// just using the keys cache
 | ||||
| 	store, err = NewYubiKeyStore(NewKeyMemoryStore(ret), ret) | ||||
| 	gottenKey, role, err := store.GetKey(privKey.ID()) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, data.CanonicalRootRole, role) | ||||
| 	assert.Equal(t, privKey.Public(), gottenKey.Public()) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue