diff --git a/client/client.go b/client/client.go index 02ba50f2a4..c8cd8a1982 100644 --- a/client/client.go +++ b/client/client.go @@ -169,7 +169,7 @@ func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...st // currently we only support server managing timestamps and snapshots, and // nothing else - timestamps are always managed by the server, and implicit // (do not have to be passed in as part of `serverManagedRoles`, so that - // the API of Initialize doens't change). + // the API of Initialize doesn't change). var serverManagesSnapshot bool locallyManagedKeys := []string{ data.CanonicalTargetsRole, @@ -396,7 +396,7 @@ func (r *NotaryRepository) RemoveDelegation(name string, keyIDs, paths []string, } // AddTarget creates new changelist entries to add a target to the given roles -// in the repository when the changelist gets appied at publish time. +// in the repository when the changelist gets applied at publish time. // If roles are unspecified, the default role is "targets". func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error { @@ -454,7 +454,7 @@ func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, erro for _, role := range roles { // we don't need to do anything special with removing role from // roles because listSubtree always processes role and only excludes - // descendent delegations that appear in roles. + // descendant delegations that appear in roles. r.listSubtree(targets, role, roles...) } @@ -571,6 +571,56 @@ func (r *NotaryRepository) GetDelegationRoles() ([]*data.Role, error) { return allDelegations, nil } +// RoleWithSignatures is a Role with its associated signatures +type RoleWithSignatures struct { + Signatures []data.Signature + data.Role +} + +// GetRepoRoleMetaInfo returns a list of RoleWithSignatures objects for this repo +// This represents the latest metadata for each role in this repo +func (r *NotaryRepository) GetRepoRoleMetaInfo() ([]RoleWithSignatures, error) { + // Update to latest repo state + _, err := r.Update(false) + if err != nil { + return nil, err + } + + // Get all role info from our updated keysDB + roles, err := r.tufRepo.GetAllRoles() + if err != nil { + return nil, err + } + + var roleWithSigs []RoleWithSignatures + + // Populate RoleWithSignatures with Role from keysDB and signatures from TUF metadata + for _, role := range roles { + roleWithSig := RoleWithSignatures{Role: *role, Signatures: nil} + switch role.Name { + case data.CanonicalRootRole: + roleWithSig.Signatures = r.tufRepo.Root.Signatures + case data.CanonicalTargetsRole: + roleWithSig.Signatures = r.tufRepo.Targets[data.CanonicalTargetsRole].Signatures + case data.CanonicalSnapshotRole: + roleWithSig.Signatures = r.tufRepo.Snapshot.Signatures + case data.CanonicalTimestampRole: + roleWithSig.Signatures = r.tufRepo.Timestamp.Signatures + default: + // If the role isn't a delegation, we should error -- this is only possible if we have invalid keyDB state + if !data.IsDelegation(role.Name) { + return nil, data.ErrInvalidRole{Role: role.Name, Reason: "invalid role name"} + } + if _, ok := r.tufRepo.Targets[role.Name]; ok { + // We'll only find a signature if we've published any targets with this delegation + roleWithSig.Signatures = r.tufRepo.Targets[role.Name].Signatures + } + } + roleWithSigs = append(roleWithSigs, roleWithSig) + } + return roleWithSigs, nil +} + // Publish pushes the local changes in signed material to the remote notary-server // Conceptually it performs an operation similar to a `git rebase` func (r *NotaryRepository) Publish() error { diff --git a/client/client_test.go b/client/client_test.go index 1d85183613..04a838e235 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1115,7 +1115,7 @@ func testListTarget(t *testing.T, rootType string) { repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) - // tests need to manually boostrap timestamp as client doesn't generate it + // tests need to manually bootstrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() assert.NoError(t, err, "error creating repository: %s", err) @@ -1171,7 +1171,7 @@ func testListTargetWithDelegates(t *testing.T, rootType string) { repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false) defer os.RemoveAll(repo.baseDir) - // tests need to manually boostrap timestamp as client doesn't generate it + // tests need to manually bootstrap timestamp as client doesn't generate it err := repo.tufRepo.InitTimestamp() assert.NoError(t, err, "error creating repository: %s", err) @@ -2949,3 +2949,102 @@ func TestDeleteRepoNoCerts(t *testing.T) { // Assert keys for this repo exist locally assertRepoHasExpectedKeys(t, repo, rootKeyID, true) } + +// Test that we get a correct map of key IDs +func TestGetRepoRoleMetaInfo(t *testing.T) { + ts := fullTestServer(t) + defer ts.Close() + + repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false) + defer os.RemoveAll(repo.baseDir) + + assert.NoError(t, repo.Publish()) + + rolesWithSigs, err := repo.GetRepoRoleMetaInfo() + assert.NoError(t, err) + + // Should only have base roles at this point + assert.Len(t, rolesWithSigs, len(data.BaseRoles)) + // Each base role should only have one key, one signature, and its key should match the signature's key + for _, role := range rolesWithSigs { + assert.Len(t, role.Signatures, 1) + assert.Len(t, role.KeyIDs, 1) + assert.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) + } + + // Create a delegation on the top level + aKey := createKey(t, repo, "user", true) + assert.NoError(t, + repo.AddDelegation("targets/a", 1, []data.PublicKey{aKey}, []string{""}), + "error creating delegation") + + assert.NoError(t, repo.Publish()) + + rolesWithSigs, err = repo.GetRepoRoleMetaInfo() + assert.NoError(t, err) + + assert.Len(t, rolesWithSigs, len(data.BaseRoles)+1) + // The delegation hasn't published any targets or metadata so it won't have a signature yet + for _, role := range rolesWithSigs { + if role.Name == "targets/a" { + assert.Nil(t, role.Signatures) + } else { + assert.Len(t, role.Signatures, 1) + assert.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) + } + assert.Len(t, role.KeyIDs, 1) + } + + addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt", "targets/a") + assert.NoError(t, repo.Publish()) + + rolesWithSigs, err = repo.GetRepoRoleMetaInfo() + assert.NoError(t, err) + + assert.Len(t, rolesWithSigs, len(data.BaseRoles)+1) + // The delegation should have a signature now + for _, role := range rolesWithSigs { + assert.Len(t, role.Signatures, 1) + assert.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) + assert.Len(t, role.KeyIDs, 1) + } + + // Create another delegation, one level further + bKey := createKey(t, repo, "user", true) + assert.NoError(t, + repo.AddDelegation("targets/a/b", 1, []data.PublicKey{bKey}, []string{""}), + "error creating delegation") + + assert.NoError(t, repo.Publish()) + + rolesWithSigs, err = repo.GetRepoRoleMetaInfo() + assert.NoError(t, err) + + assert.Len(t, rolesWithSigs, len(data.BaseRoles)+2) + // The nested delegation hasn't published any targets or metadata so it won't have a signature yet + for _, role := range rolesWithSigs { + if role.Name == "targets/a/b" { + assert.Nil(t, role.Signatures) + } else { + assert.Len(t, role.Signatures, 1) + assert.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0]) + } + assert.Len(t, role.KeyIDs, 1) + } + + // Now make another repo and check that we don't pick up its roles + repo2, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false) + defer os.RemoveAll(repo2.baseDir) + + assert.NoError(t, repo2.Publish()) + + // repo2 only has the base roles + rolesWithSigs2, err := repo2.GetRepoRoleMetaInfo() + assert.NoError(t, err) + assert.Len(t, rolesWithSigs2, len(data.BaseRoles)) + + // original repo stays in same state (base roles + 2 delegations) + rolesWithSigs, err = repo.GetRepoRoleMetaInfo() + assert.NoError(t, err) + assert.Len(t, rolesWithSigs, len(data.BaseRoles)+2) +} diff --git a/tuf/data/roles.go b/tuf/data/roles.go index a505c92304..c323c25c61 100644 --- a/tuf/data/roles.go +++ b/tuf/data/roles.go @@ -37,6 +37,13 @@ func (e ErrNoSuchRole) Error() string { return fmt.Sprintf("role does not exist: %s", e.Role) } +// ErrNoRoles indicates no roles exist for this repo +type ErrNoRoles struct{} + +func (e ErrNoRoles) Error() string { + return fmt.Sprintf("no roles exist") +} + // ErrInvalidRole represents an error regarding a role. Typically // something like a role for which sone of the public keys were // not found in the TUF repo. diff --git a/tuf/keys/db.go b/tuf/keys/db.go index 2e27d052cd..92d2ef863e 100644 --- a/tuf/keys/db.go +++ b/tuf/keys/db.go @@ -58,6 +58,15 @@ func (db *KeyDB) AddRole(r *data.Role) error { return nil } +// GetAllRoles gets all roles from the database +func (db *KeyDB) GetAllRoles() []*data.Role { + roles := []*data.Role{} + for _, role := range db.roles { + roles = append(roles, role) + } + return roles +} + // GetKey pulls a key out of the database by its ID func (db *KeyDB) GetKey(id string) data.PublicKey { return db.keys[id] diff --git a/tuf/tuf.go b/tuf/tuf.go index 83e49467fb..4b5b686e02 100644 --- a/tuf/tuf.go +++ b/tuf/tuf.go @@ -173,6 +173,15 @@ func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error { return nil } +// GetAllRoles returns a list of all role entries for this TUF repo +func (tr *Repo) GetAllRoles() ([]*data.Role, error) { + roles := tr.keysDB.GetAllRoles() + if len(roles) == 0 { + return nil, data.ErrNoRoles{} + } + return roles, nil +} + // GetDelegation finds the role entry representing the provided // role name or ErrInvalidRole func (tr *Repo) GetDelegation(role string) (*data.Role, error) { diff --git a/tuf/tuf_test.go b/tuf/tuf_test.go index 3f297778fa..d75a583bbd 100644 --- a/tuf/tuf_test.go +++ b/tuf/tuf_test.go @@ -936,3 +936,19 @@ func TestAddBaseKeysToRoot(t *testing.T) { } } } + +func TestGetAllRoles(t *testing.T) { + ed25519 := signed.NewEd25519() + keyDB := keys.NewDB() + repo := initRepo(t, ed25519, keyDB) + + // After we init, we get the base roles + roles, err := repo.GetAllRoles() + assert.Len(t, roles, len(data.BaseRoles)) + + // Clear the keysDB, check that we error + repo.keysDB = keys.NewDB() + roles, err = repo.GetAllRoles() + assert.Error(t, err) + assert.IsType(t, data.ErrNoRoles{}, err) +}