Skip to content
This repository was archived by the owner on Jul 31, 2025. It is now read-only.

Commit 8f0965c

Browse files
authored
Merge pull request #981 from cyli/read-flattened-keystore
Be able to read keys even if they are not in the root/non-root subdirs
2 parents f8c0b01 + 128bf0c commit 8f0965c

File tree

3 files changed

+130
-99
lines changed

3 files changed

+130
-99
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
+ Preliminary Windows support for notary client [#970](https://github.com/docker/notary/pull/970)
55
+ Output message to CLI when repo changes have been successfully published [#974](https://github.com/docker/notary/pull/974)
66
+ Improved error messages for client authentication errors and for the witness command [#972](https://github.com/docker/notary/pull/972)
7+
+ Support for finding keys that are anywhere in the notary directory's "private" directory, not just under "private/root_keys" or "private/tuf_keys" [#981](https://github.com/docker/notary/pull/981)
78

89
## [v0.4.0](https://github.com/docker/notary/releases/tag/v0.4.0) 9/21/2016
910
+ Server-managed key rotations [#889](https://github.com/docker/notary/pull/889)

trustmanager/keystore.go

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -173,26 +173,12 @@ func (s *GenericKeyStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error
173173
func (s *GenericKeyStore) GetKey(name string) (data.PrivateKey, string, error) {
174174
s.Lock()
175175
defer s.Unlock()
176-
// If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds
177-
if keyInfo, ok := s.keyInfoMap[name]; ok {
178-
name = filepath.Join(keyInfo.Gun, name)
179-
}
180-
181176
cachedKeyEntry, ok := s.cachedKeys[name]
182177
if ok {
183178
return cachedKeyEntry.key, cachedKeyEntry.alias, nil
184179
}
185180

186-
keyAlias, legacy, err := getKeyRole(s.store, name)
187-
if err != nil {
188-
return nil, "", err
189-
}
190-
191-
if legacy {
192-
name = name + "_" + keyAlias
193-
}
194-
195-
keyBytes, err := s.store.Get(filepath.Join(getSubdir(keyAlias), name))
181+
keyBytes, _, keyAlias, err := getKey(s.store, name)
196182
if err != nil {
197183
return nil, "", err
198184
}
@@ -218,25 +204,18 @@ func (s *GenericKeyStore) ListKeys() map[string]KeyInfo {
218204
func (s *GenericKeyStore) RemoveKey(keyID string) error {
219205
s.Lock()
220206
defer s.Unlock()
221-
// If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds
222-
if keyInfo, ok := s.keyInfoMap[keyID]; ok {
223-
keyID = filepath.Join(keyInfo.Gun, keyID)
224-
}
225207

226-
role, legacy, err := getKeyRole(s.store, keyID)
227-
if err != nil {
208+
_, filename, _, err := getKey(s.store, keyID)
209+
switch err.(type) {
210+
case ErrKeyNotFound, nil:
211+
break
212+
default:
228213
return err
229214
}
230215

231216
delete(s.cachedKeys, keyID)
232217

233-
name := keyID
234-
if legacy {
235-
name = keyID + "_" + role
236-
}
237-
238-
// being in a subdirectory is for backwards compatibliity
239-
err = s.store.Remove(filepath.Join(getSubdir(role), name))
218+
err = s.store.Remove(filename) // removing a file that doesn't exist doesn't fail
240219
if err != nil {
241220
return err
242221
}
@@ -276,11 +255,11 @@ func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) {
276255
return keyID, KeyInfo{Gun: gun, Role: role}, nil
277256
}
278257

279-
// getKeyRole finds the role for the given keyID. It attempts to look
280-
// both in the newer format PEM headers, and also in the legacy filename
281-
// format. It returns: the role, whether it was found in the legacy format
282-
// (true == legacy), and an error
283-
func getKeyRole(s Storage, keyID string) (string, bool, error) {
258+
// getKey finds the key and role for the given keyID. It attempts to
259+
// look both in the newer format PEM headers, and also in the legacy filename
260+
// format. It returns: the key bytes, the filename it was found under, the role,
261+
// and an error
262+
func getKey(s Storage, keyID string) ([]byte, string, string, error) {
284263
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
285264

286265
for _, file := range s.ListFiles() {
@@ -289,21 +268,21 @@ func getKeyRole(s Storage, keyID string) (string, bool, error) {
289268
if strings.HasPrefix(filename, name) {
290269
d, err := s.Get(file)
291270
if err != nil {
292-
return "", false, err
271+
return nil, "", "", err
293272
}
294273
block, _ := pem.Decode(d)
295274
if block != nil {
296275
if role, ok := block.Headers["role"]; ok {
297-
return role, false, nil
276+
return d, file, role, nil
298277
}
299278
}
300279

301280
role := strings.TrimPrefix(filename, name+"_")
302-
return role, true, nil
281+
return d, file, role, nil
303282
}
304283
}
305284

306-
return "", false, ErrKeyNotFound{KeyID: keyID}
285+
return nil, "", "", ErrKeyNotFound{KeyID: keyID}
307286
}
308287

309288
// Assumes 2 subdirectories, 1 containing root keys and 1 containing TUF keys

trustmanager/keystore_test.go

Lines changed: 113 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package trustmanager
22

33
import (
44
"crypto/rand"
5+
"encoding/pem"
56
"errors"
67
"fmt"
78
"io/ioutil"
89
"os"
10+
"path"
911
"path/filepath"
1012
"testing"
1113

@@ -167,29 +169,39 @@ func TestGet(t *testing.T) {
167169

168170
gun := "docker.io/notary"
169171

170-
// Root role needs to go in the rootKeySubdir to be read.
171-
// All other roles can go in the nonRootKeysSubdir, possibly under a GUN
172+
// Root role currently goes into the rootKeySubdir, and all other roles go
173+
// in the nonRootKeysSubdir, possibly under a GUN.
172174
nonRootKeysSubdirWithGUN := filepath.Clean(filepath.Join(notary.NonRootKeysSubdir, gun))
173-
174-
testGetKeyWithRole(t, "", data.CanonicalRootRole, notary.RootKeysSubdir, true)
175+
testGetKeyWithRole(t, "", data.CanonicalRootRole, filepath.Join(notary.PrivDir, notary.RootKeysSubdir), true)
175176
for _, role := range nonRootRolesToTest {
176-
testGetKeyWithRole(t, "", role, notary.NonRootKeysSubdir, true)
177-
testGetKeyWithRole(t, gun, role, nonRootKeysSubdirWithGUN, true)
177+
testGetKeyWithRole(t, "", role, filepath.Join(notary.PrivDir, notary.NonRootKeysSubdir), true)
178+
testGetKeyWithRole(t, gun, role, filepath.Join(notary.PrivDir, nonRootKeysSubdirWithGUN), true)
178179
}
179180

180-
// Root cannot go in the nonRootKeysSubdir, or it won't be able to be read,
181-
// and vice versa
182-
testGetKeyWithRole(t, "", data.CanonicalRootRole, notary.NonRootKeysSubdir, false)
183-
testGetKeyWithRole(t, gun, data.CanonicalRootRole, nonRootKeysSubdirWithGUN, false)
184-
for _, role := range nonRootRolesToTest {
185-
testGetKeyWithRole(t, "", role, notary.RootKeysSubdir, false)
181+
// However, keys of any role can be read from anywhere in the private dir so long as
182+
// it has the right ID
183+
for _, expectedSubdir := range []string{
184+
notary.PrivDir,
185+
filepath.Join(notary.PrivDir, nonRootKeysSubdirWithGUN),
186+
filepath.Join(notary.PrivDir, notary.RootKeysSubdir),
187+
filepath.Join(notary.PrivDir, notary.RootKeysSubdir, gun),
188+
filepath.Join(notary.PrivDir, notary.NonRootKeysSubdir),
189+
} {
190+
for _, role := range append(nonRootRolesToTest, data.CanonicalRootRole) {
191+
testGetKeyWithRole(t, "", role, expectedSubdir, true)
192+
testGetKeyWithRole(t, gun, role, expectedSubdir, true)
193+
}
186194
}
187-
}
188195

189-
func testGetKeyWithRole(t *testing.T, gun, role, expectedSubdir string, success bool) {
190-
testData := []byte(fmt.Sprintf(`-----BEGIN RSA PRIVATE KEY-----
191-
role: %s
196+
// keys outside of the private dir cannot be read
197+
for _, role := range append(nonRootRolesToTest, data.CanonicalRootRole) {
198+
testGetKeyWithRole(t, "", role, "otherDir", false)
199+
testGetKeyWithRole(t, gun, role, "otherDir", false)
200+
}
201+
}
192202

203+
func writeKeyFile(t *testing.T, perms os.FileMode, filename, roleInPEM string) []byte {
204+
testData := []byte(`-----BEGIN RSA PRIVATE KEY-----
193205
MIIEogIBAAKCAQEAyUIXjsrWRrvPa4Bzp3VJ6uOUGPay2fUpSV8XzNxZxIG/Opdr
194206
+k3EQi1im6WOqF3Y5AS1UjYRxNuRN+cAZeo3uS1pOTuoSupBXuchVw8s4hZJ5vXn
195207
TRmGb+xY7tZ1ZVgPfAZDib9sRSUsL/gC+aSyprAjG/YBdbF06qKbfOfsoCEYW1OQ
@@ -216,7 +228,24 @@ EkqpAoGAJWe8PC0XK2RE9VkbSPg9Ehr939mOLWiHGYTVWPttUcum/rTKu73/X/mj
216228
WxnPWGtzM1pHWypSokW90SP4/xedMxludvBvmz+CTYkNJcBGCrJumy11qJhii9xp
217229
EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0=
218230
-----END RSA PRIVATE KEY-----
219-
`, role))
231+
`)
232+
233+
if roleInPEM != "" {
234+
block, _ := pem.Decode(testData)
235+
require.NotNil(t, block)
236+
block.Headers = map[string]string{
237+
"role": roleInPEM,
238+
}
239+
testData = pem.EncodeToMemory(block)
240+
}
241+
242+
os.MkdirAll(filepath.Dir(filename), perms)
243+
err := ioutil.WriteFile(filename, testData, perms)
244+
require.NoError(t, err, "failed to write test file")
245+
return testData
246+
}
247+
248+
func testGetKeyWithRole(t *testing.T, gun, role, expectedSubdir string, success bool) {
220249
testName := "keyID"
221250
testExt := "key"
222251
perms := os.FileMode(0755)
@@ -229,18 +258,16 @@ EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0=
229258
defer os.RemoveAll(tempBaseDir)
230259

231260
// Since we're generating this manually we need to add the extension '.'
232-
filePath := filepath.Join(tempBaseDir, notary.PrivDir, expectedSubdir, testName+"."+testExt)
233-
os.MkdirAll(filepath.Dir(filePath), perms)
234-
err = ioutil.WriteFile(filePath, testData, perms)
235-
require.NoError(t, err, "failed to write test file")
261+
filePath := filepath.Join(tempBaseDir, expectedSubdir, testName+"."+testExt)
262+
testData := writeKeyFile(t, perms, filePath, role)
236263

237264
// Create our store
238265
store, err := NewKeyFileStore(tempBaseDir, emptyPassphraseRetriever)
239266
require.NoError(t, err, "failed to create new key filestore")
240267

241268
// Call the GetKey function
242269
if gun != "" {
243-
testName = gun + "/keyID"
270+
testName = path.Join(gun, "keyID")
244271
}
245272
privKey, _, err := store.GetKey(testName)
246273
if success {
@@ -263,34 +290,6 @@ EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0=
263290
// TestGetLegacyKey ensures we can still load keys where the role
264291
// is stored as part of the filename (i.e. <hexID>_<role>.key
265292
func TestGetLegacyKey(t *testing.T) {
266-
testData := []byte(`-----BEGIN RSA PRIVATE KEY-----
267-
MIIEogIBAAKCAQEAyUIXjsrWRrvPa4Bzp3VJ6uOUGPay2fUpSV8XzNxZxIG/Opdr
268-
+k3EQi1im6WOqF3Y5AS1UjYRxNuRN+cAZeo3uS1pOTuoSupBXuchVw8s4hZJ5vXn
269-
TRmGb+xY7tZ1ZVgPfAZDib9sRSUsL/gC+aSyprAjG/YBdbF06qKbfOfsoCEYW1OQ
270-
82JqHzQH514RFYPTnEGpvfxWaqmFQLmv0uMxV/cAYvqtrGkXuP0+a8PknlD2obw5
271-
0rHE56Su1c3Q42S7L51K38tpbgWOSRcTfDUWEj5v9wokkNQvyKBwbS996s4EJaZd
272-
7r6M0h1pHnuRxcSaZLYRwgOe1VNGg2VfWzgd5QIDAQABAoIBAF9LGwpygmj1jm3R
273-
YXGd+ITugvYbAW5wRb9G9mb6wspnwNsGTYsz/UR0ZudZyaVw4jx8+jnV/i3e5PC6
274-
QRcAgqf8l4EQ/UuThaZg/AlT1yWp9g4UyxNXja87EpTsGKQGwTYxZRM4/xPyWOzR
275-
mt8Hm8uPROB9aA2JG9npaoQG8KSUj25G2Qot3ukw/IOtqwN/Sx1EqF0EfCH1K4KU
276-
a5TrqlYDFmHbqT1zTRec/BTtVXNsg8xmF94U1HpWf3Lpg0BPYT7JiN2DPoLelRDy
277-
a/A+a3ZMRNISL5wbq/jyALLOOyOkIqa+KEOeW3USuePd6RhDMzMm/0ocp5FCwYfo
278-
k4DDeaECgYEA0eSMD1dPGo+u8UTD8i7ZsZCS5lmXLNuuAg5f5B/FGghD8ymPROIb
279-
dnJL5QSbUpmBsYJ+nnO8RiLrICGBe7BehOitCKi/iiZKJO6edrfNKzhf4XlU0HFl
280-
jAOMa975pHjeCoZ1cXJOEO9oW4SWTCyBDBSqH3/ZMgIOiIEk896lSmkCgYEA9Xf5
281-
Jqv3HtQVvjugV/axAh9aI8LMjlfFr9SK7iXpY53UdcylOSWKrrDok3UnrSEykjm7
282-
UL3eCU5jwtkVnEXesNn6DdYo3r43E6iAiph7IBkB5dh0yv3vhIXPgYqyTnpdz4pg
283-
3yPGBHMPnJUBThg1qM7k6a2BKHWySxEgC1DTMB0CgYAGvdmF0J8Y0k6jLzs/9yNE
284-
4cjmHzCM3016gW2xDRgumt9b2xTf+Ic7SbaIV5qJj6arxe49NqhwdESrFohrKaIP
285-
kM2l/o2QaWRuRT/Pvl2Xqsrhmh0QSOQjGCYVfOb10nAHVIRHLY22W4o1jk+piLBo
286-
a+1+74NRaOGAnu1J6/fRKQKBgAF180+dmlzemjqFlFCxsR/4G8s2r4zxTMXdF+6O
287-
3zKuj8MbsqgCZy7e8qNeARxwpCJmoYy7dITNqJ5SOGSzrb2Trn9ClP+uVhmR2SH6
288-
AlGQlIhPn3JNzI0XVsLIloMNC13ezvDE/7qrDJ677EQQtNEKWiZh1/DrsmHr+irX
289-
EkqpAoGAJWe8PC0XK2RE9VkbSPg9Ehr939mOLWiHGYTVWPttUcum/rTKu73/X/mj
290-
WxnPWGtzM1pHWypSokW90SP4/xedMxludvBvmz+CTYkNJcBGCrJumy11qJhii9xp
291-
EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0=
292-
-----END RSA PRIVATE KEY-----
293-
`)
294293
testName := "docker.com/notary/root"
295294
testExt := "key"
296295
testAlias := "root"
@@ -305,10 +304,7 @@ EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0=
305304

306305
// Since we're generating this manually we need to add the extension '.'
307306
filePath := filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir, testName+"_"+testAlias+"."+testExt)
308-
309-
os.MkdirAll(filepath.Dir(filePath), perms)
310-
err = ioutil.WriteFile(filePath, testData, perms)
311-
require.NoError(t, err, "failed to write test file")
307+
writeKeyFile(t, perms, filePath, "")
312308

313309
// Create our store
314310
store, err := NewKeyFileStore(tempBaseDir, emptyPassphraseRetriever)
@@ -356,12 +352,6 @@ func TestListKeys(t *testing.T) {
356352
require.Equal(t, role, listedInfo.Role)
357353
}
358354

359-
// Write an invalid filename to the directory
360-
filePath := filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir, "fakekeyname.key")
361-
err = ioutil.WriteFile(filePath, []byte("data"), perms)
362-
require.NoError(t, err, "failed to write test file")
363-
364-
// Check to see if the keystore still lists two keys
365355
keyMap := store.ListKeys()
366356
require.Len(t, keyMap, len(roles))
367357

@@ -372,6 +362,31 @@ func TestListKeys(t *testing.T) {
372362
_, err = store.GetKeyInfo(keyID)
373363
require.NoError(t, err)
374364
}
365+
366+
require.Len(t, store.ListKeys(), len(roles))
367+
368+
// ListKeys() works even if the keys are in non-standard locations
369+
for i, loc := range []string{
370+
filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir),
371+
filepath.Join(tempBaseDir, notary.PrivDir, notary.NonRootKeysSubdir),
372+
filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir, testName),
373+
filepath.Join(tempBaseDir, notary.PrivDir, testName),
374+
filepath.Join(tempBaseDir, notary.PrivDir),
375+
tempBaseDir, // this key won't be read
376+
} {
377+
fp := filepath.Join(loc, fmt.Sprintf("keyID%d.key", i))
378+
writeKeyFile(t, perms, fp, "")
379+
380+
// Ensure file exists
381+
_, err := ioutil.ReadFile(fp)
382+
require.NoError(t, err, "expected file not found")
383+
}
384+
385+
// update our store so we read from the FS again
386+
store, err = NewKeyFileStore(tempBaseDir, passphraseRetriever)
387+
require.NoError(t, err, "failed to create new key filestore")
388+
389+
require.Len(t, store.ListKeys(), len(roles)+5)
375390
}
376391

377392
func TestAddGetKeyMemStore(t *testing.T) {
@@ -567,6 +582,42 @@ func TestRemoveKey(t *testing.T) {
567582
testRemoveKeyWithRole(t, data.CanonicalSnapshotRole, filepath.Join(notary.NonRootKeysSubdir, gun))
568583
testRemoveKeyWithRole(t, "targets/a/b/c", notary.NonRootKeysSubdir)
569584
testRemoveKeyWithRole(t, "invalidRole", notary.NonRootKeysSubdir)
585+
586+
// create another store for other testing
587+
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
588+
require.NoError(t, err, "failed to create a temporary directory")
589+
defer os.RemoveAll(tempBaseDir)
590+
591+
store, err := NewKeyFileStore(tempBaseDir, passphraseRetriever)
592+
require.NoError(t, err, "failed to create new key filestore")
593+
594+
// write keys to non-standard locations - since we're generating keys manually
595+
// we need to add the extenxion
596+
perms := os.FileMode(0755)
597+
for i, loc := range []string{
598+
filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir),
599+
filepath.Join(tempBaseDir, notary.PrivDir, notary.NonRootKeysSubdir),
600+
filepath.Join(tempBaseDir, notary.PrivDir, notary.RootKeysSubdir, gun),
601+
filepath.Join(tempBaseDir, notary.PrivDir, gun),
602+
filepath.Join(tempBaseDir, notary.PrivDir),
603+
} {
604+
fp := filepath.Join(loc, fmt.Sprintf("keyID%d.key", i))
605+
writeKeyFile(t, perms, fp, "")
606+
607+
// Ensure file exists
608+
_, err := ioutil.ReadFile(fp)
609+
require.NoError(t, err, "expected file not found")
610+
611+
err = store.RemoveKey(fmt.Sprintf("keyID%d", i))
612+
require.NoError(t, err)
613+
614+
// File should no longer exist
615+
_, err = ioutil.ReadFile(fp)
616+
require.True(t, os.IsNotExist(err), "file should not exist")
617+
}
618+
619+
// removing a non-existent key should not error
620+
require.NoError(t, store.RemoveKey("nope"))
570621
}
571622

572623
func testRemoveKeyWithRole(t *testing.T, role, expectedSubdir string) {
@@ -599,9 +650,9 @@ func testRemoveKeyWithRole(t *testing.T, role, expectedSubdir string) {
599650
err = store.RemoveKey(privKey.ID())
600651
require.NoError(t, err, "unable to remove key")
601652

602-
// Check to see if file still exists
653+
// File should no longer exist
603654
_, err = ioutil.ReadFile(expectedFilePath)
604-
require.Error(t, err, "file should not exist")
655+
require.True(t, os.IsNotExist(err), "file should not exist")
605656
}
606657

607658
func TestKeysAreCached(t *testing.T) {

0 commit comments

Comments
 (0)