docs/client/client_test.go

3525 lines
129 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package client
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"math"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"time"
"github.com/Sirupsen/logrus"
ctxu "github.com/docker/distribution/context"
"github.com/docker/go/canonical/json"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"github.com/docker/notary"
"github.com/docker/notary/client/changelist"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/server"
"github.com/docker/notary/server/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
"github.com/docker/notary/tuf/utils"
"github.com/docker/notary/tuf/validation"
)
const password = "passphrase"
type passRoleRecorder struct {
rolesCreated []string
rolesAsked []string
}
func newRoleRecorder() *passRoleRecorder {
return &passRoleRecorder{}
}
func (p *passRoleRecorder) clear() {
p.rolesCreated = nil
p.rolesAsked = nil
}
func (p *passRoleRecorder) retriever(_, alias string, createNew bool, _ int) (string, bool, error) {
if createNew {
p.rolesCreated = append(p.rolesCreated, alias)
} else {
p.rolesAsked = append(p.rolesAsked, alias)
}
return password, false, nil
}
func (p *passRoleRecorder) compareRolesRecorded(t *testing.T, expected []string, created bool,
args ...interface{}) {
var actual, useExpected sort.StringSlice
copy(expected, useExpected) // don't sort expected, since we don't want to mutate it
sort.Stable(useExpected)
if created {
copy(p.rolesCreated, actual)
} else {
copy(p.rolesAsked, actual)
}
sort.Stable(actual)
require.Equal(t, useExpected, actual, args...)
}
// requires the following keys be created: order does not matter
func (p *passRoleRecorder) requireCreated(t *testing.T, expected []string, args ...interface{}) {
p.compareRolesRecorded(t, expected, true, args...)
}
// requires that passwords be asked for the following keys: order does not matter
func (p *passRoleRecorder) requireAsked(t *testing.T, expected []string, args ...interface{}) {
p.compareRolesRecorded(t, expected, false, args...)
}
var passphraseRetriever = passphrase.ConstantRetriever(password)
func simpleTestServer(t *testing.T, roles ...string) (
*httptest.Server, *http.ServeMux, map[string]data.PrivateKey) {
if len(roles) == 0 {
roles = []string{data.CanonicalTimestampRole, data.CanonicalSnapshotRole}
}
keys := make(map[string]data.PrivateKey)
mux := http.NewServeMux()
for _, role := range roles {
key, err := trustmanager.GenerateECDSAKey(rand.Reader)
require.NoError(t, err)
keys[role] = key
pubKey := data.PublicKeyFromPrivate(key)
jsonBytes, err := json.MarshalCanonical(&pubKey)
require.NoError(t, err)
keyJSON := string(jsonBytes)
// TUF will request /v2/docker.com/notary/_trust/tuf/<role>.key
mux.HandleFunc(
fmt.Sprintf("/v2/docker.com/notary/_trust/tuf/%s.key", role),
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, keyJSON)
})
}
ts := httptest.NewServer(mux)
return ts, mux, keys
}
func fullTestServer(t *testing.T) *httptest.Server {
// Set up server
ctx := context.WithValue(
context.Background(), "metaStore", storage.NewMemStorage())
// Do not pass one of the const KeyAlgorithms here as the value! Passing a
// string is in itself good test that we are handling it correctly as we
// will be receiving a string from the configuration.
ctx = context.WithValue(ctx, "keyAlgorithm", "ecdsa")
// Eat the logs instead of spewing them out
var b bytes.Buffer
l := logrus.New()
l.Out = &b
ctx = ctxu.WithLogger(ctx, logrus.NewEntry(l))
cryptoService := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever))
return httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil, nil))
}
// server that returns some particular error code all the time
func errorTestServer(t *testing.T, errorCode int) *httptest.Server {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(errorCode)
}
server := httptest.NewServer(http.HandlerFunc(handler))
return server
}
// initializes a repository in a temporary directory
func initializeRepo(t *testing.T, rootType, gun, url string,
serverManagesSnapshot bool) (*NotaryRepository, string) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
serverManagedRoles := []string{}
if serverManagesSnapshot {
serverManagedRoles = []string{data.CanonicalSnapshotRole}
}
repo, rec, rootPubKeyID := createRepoAndKey(t, rootType, tempBaseDir, gun, url)
err = repo.Initialize(rootPubKeyID, serverManagedRoles...)
if err != nil {
os.RemoveAll(tempBaseDir)
}
require.NoError(t, err, "error creating repository: %s", err)
// generates the target role, maybe the snapshot role
if serverManagesSnapshot {
rec.requireCreated(t, []string{data.CanonicalTargetsRole})
} else {
rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
}
// root key is cached by the cryptoservice, so when signing we don't actually ask
// for the passphrase
rec.requireAsked(t, nil)
return repo, rootPubKeyID
}
// Creates a new repository and adds a root key. Returns the repo and key ID.
func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) (
*NotaryRepository, *passRoleRecorder, string) {
rec := newRoleRecorder()
repo, err := NewNotaryRepository(
tempBaseDir, gun, url, http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType)
require.NoError(t, err, "error generating root key: %s", err)
rec.requireCreated(t, []string{data.CanonicalRootRole},
"root passphrase should have been required to generate a root key")
rec.requireAsked(t, nil)
rec.clear()
return repo, rec, rootPubKey.ID()
}
// creates a new notary repository with the same gun and url as the previous
// repo, in order to eliminate caches (for instance, cryptoservice cache)
// if a new directory is to be created, it also eliminates the TUF metadata
// cache
func newRepoToTestRepo(t *testing.T, existingRepo *NotaryRepository, newDir bool) (
*NotaryRepository, *passRoleRecorder) {
repoDir := existingRepo.baseDir
if newDir {
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
repoDir = tempBaseDir
}
rec := newRoleRecorder()
repo, err := NewNotaryRepository(
repoDir, existingRepo.gun, existingRepo.baseURL,
http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repository: %s", err)
if err != nil && newDir {
defer os.RemoveAll(repoDir)
}
return repo, rec
}
// Initializing a new repo while specifying that the server should manage the root
// role will fail.
func TestInitRepositoryManagedRolesIncludingRoot(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost")
err = repo.Initialize(rootPubKeyID, data.CanonicalRootRole)
require.Error(t, err)
require.IsType(t, ErrInvalidRemoteRole{}, err)
// Just testing the error message here in this one case
require.Equal(t, err.Error(),
"notary does not permit the server managing the root key")
// no key creation happened
rec.requireCreated(t, nil)
}
// Initializing a new repo while specifying that the server should manage some
// invalid role will fail.
func TestInitRepositoryManagedRolesInvalidRole(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost")
err = repo.Initialize(rootPubKeyID, "randomrole")
require.Error(t, err)
require.IsType(t, ErrInvalidRemoteRole{}, err)
// no key creation happened
rec.requireCreated(t, nil)
}
// Initializing a new repo while specifying that the server should manage the
// targets role will fail.
func TestInitRepositoryManagedRolesIncludingTargets(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", "http://localhost")
err = repo.Initialize(rootPubKeyID, data.CanonicalTargetsRole)
require.Error(t, err)
require.IsType(t, ErrInvalidRemoteRole{}, err)
// no key creation happened
rec.requireCreated(t, nil)
}
// Initializing a new repo while specifying that the server should manage the
// timestamp key is fine - that's what it already does, so no error.
func TestInitRepositoryManagedRolesIncludingTimestamp(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL)
err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole)
require.NoError(t, err)
// generates the target role, the snapshot role
rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
}
// Initializing a new repo fails if unable to get the timestamp key, even if
// the snapshot key is available
func TestInitRepositoryNeedsRemoteTimestampKey(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
ts, _, _ := simpleTestServer(t, data.CanonicalSnapshotRole)
defer ts.Close()
repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL)
err = repo.Initialize(rootPubKeyID, data.CanonicalTimestampRole)
require.Error(t, err)
require.IsType(t, store.ErrMetaNotFound{}, err)
// locally managed keys are created first, to avoid unnecssary network calls,
// so they would have been generated
rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
}
// Initializing a new repo with remote server signing fails if unable to get
// the snapshot key, even if the timestamp key is available
func TestInitRepositoryNeedsRemoteSnapshotKey(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory")
defer os.RemoveAll(tempBaseDir)
ts, _, _ := simpleTestServer(t, data.CanonicalTimestampRole)
defer ts.Close()
repo, rec, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL)
err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole)
require.Error(t, err)
require.IsType(t, store.ErrMetaNotFound{}, err)
// locally managed keys are created first, to avoid unnecssary network calls,
// so they would have been generated
rec.requireCreated(t, []string{data.CanonicalTargetsRole})
}
// passing timestamp + snapshot, or just snapshot, is tested in the next two
// test cases.
// TestInitRepoServerOnlyManagesTimestampKey runs through the process of
// initializing a repository and makes sure the repository looks correct on disk.
// We test this with both an RSA and ECDSA root key.
// This test case covers the default case where the server only manages the
// timestamp key.
func TestInitRepoServerOnlyManagesTimestampKey(t *testing.T) {
testInitRepoMetadata(t, data.ECDSAKey, false)
testInitRepoSigningKeys(t, data.ECDSAKey, false)
if !testing.Short() {
testInitRepoMetadata(t, data.RSAKey, false)
testInitRepoSigningKeys(t, data.RSAKey, false)
}
}
// TestInitRepoServerManagesTimestampAndSnapshotKeys runs through the process of
// initializing a repository and makes sure the repository looks correct on disk.
// We test this with both an RSA and ECDSA root key.
// This test case covers the server managing both the timestap and snapshot keys.
func TestInitRepoServerManagesTimestampAndSnapshotKeys(t *testing.T) {
testInitRepoMetadata(t, data.ECDSAKey, true)
testInitRepoSigningKeys(t, data.ECDSAKey, true)
if !testing.Short() {
testInitRepoMetadata(t, data.RSAKey, true)
testInitRepoSigningKeys(t, data.RSAKey, false)
}
}
// This creates a new KeyFileStore in the repo's base directory and makes sure
// the repo has the right number of keys
func requireRepoHasExpectedKeys(t *testing.T, repo *NotaryRepository,
rootKeyID string, expectedSnapshotKey bool) {
// The repo should have a keyFileStore and have created keys using it,
// so create a new KeyFileStore, and check that the keys do exist and are
// valid
ks, err := trustmanager.NewKeyFileStore(repo.baseDir, passphraseRetriever)
require.NoError(t, err)
roles := make(map[string]bool)
for keyID, keyInfo := range ks.ListKeys() {
if keyInfo.Role == data.CanonicalRootRole {
require.Equal(t, rootKeyID, keyID, "Unexpected root key ID")
}
// just to ensure the content of the key files created are valid
_, r, err := ks.GetKey(keyID)
require.NoError(t, err)
require.Equal(t, keyInfo.Role, r)
roles[keyInfo.Role] = true
}
// there is a root key and a targets key
alwaysThere := []string{data.CanonicalRootRole, data.CanonicalTargetsRole}
for _, role := range alwaysThere {
_, ok := roles[role]
require.True(t, ok, "missing %s key", role)
}
// there may be a snapshots key, depending on whether the server is managing
// the snapshots key
_, ok := roles[data.CanonicalSnapshotRole]
if expectedSnapshotKey {
require.True(t, ok, "missing snapshot key")
} else {
require.False(t, ok,
"there should be no snapshot key because the server manages it")
}
// The server manages the timestamp key - there should not be a timestamp
// key
_, ok = roles[data.CanonicalTimestampRole]
require.False(t, ok,
"there should be no timestamp key because the server manages it")
}
// Sanity check the TUF metadata files. Verify that it exists for a particular
// role, the JSON is well-formed, and the signatures exist.
// For the root.json file, also check that the root, snapshot, and
// targets key IDs are present.
func requireRepoHasExpectedMetadata(t *testing.T, repo *NotaryRepository,
role string, expected bool) {
filename := filepath.Join(tufDir, filepath.FromSlash(repo.gun),
"metadata", role+".json")
fullPath := filepath.Join(repo.baseDir, filename)
_, err := os.Stat(fullPath)
if expected {
require.NoError(t, err, "missing TUF metadata file: %s", filename)
} else {
require.Error(t, err,
"%s metadata should not exist, but does: %s", role, filename)
return
}
jsonBytes, err := ioutil.ReadFile(fullPath)
require.NoError(t, err, "error reading TUF metadata file %s: %s", filename, err)
var decoded data.Signed
err = json.Unmarshal(jsonBytes, &decoded)
require.NoError(t, err, "error parsing TUF metadata file %s: %s", filename, err)
require.Len(t, decoded.Signatures, 1,
"incorrect number of signatures in TUF metadata file %s", filename)
require.NotEmpty(t, decoded.Signatures[0].KeyID,
"empty key ID field in TUF metadata file %s", filename)
require.NotEmpty(t, decoded.Signatures[0].Method,
"empty method field in TUF metadata file %s", filename)
require.NotEmpty(t, decoded.Signatures[0].Signature,
"empty signature in TUF metadata file %s", filename)
// Special case for root.json: also check that the signed
// content for keys and roles
if role == data.CanonicalRootRole {
var decodedRoot data.Root
err := json.Unmarshal(*decoded.Signed, &decodedRoot)
require.NoError(t, err, "error parsing root.json signed section: %s", err)
require.Equal(t, "Root", decodedRoot.Type, "_type mismatch in root.json")
// Expect 1 key for each valid role in the Keys map - one for
// each of root, targets, snapshot, timestamp
require.Len(t, decodedRoot.Keys, len(data.BaseRoles),
"wrong number of keys in root.json")
require.True(t, len(decodedRoot.Roles) >= len(data.BaseRoles),
"wrong number of roles in root.json")
for _, role := range data.BaseRoles {
_, ok := decodedRoot.Roles[role]
require.True(t, ok, "Missing role %s in root.json", role)
}
}
}
func testInitRepoMetadata(t *testing.T, rootType string, serverManagesSnapshot bool) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, rootType, gun, ts.URL, serverManagesSnapshot)
defer os.RemoveAll(repo.baseDir)
requireRepoHasExpectedKeys(t, repo, rootKeyID, !serverManagesSnapshot)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole,
!serverManagesSnapshot)
}
func testInitRepoSigningKeys(t *testing.T, rootType string, serverManagesSnapshot bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
repo, _, rootPubKeyID := createRepoAndKey(
t, data.ECDSAKey, tempBaseDir, "docker.com/notary", ts.URL)
// create a new repository, so we can wipe out the cryptoservice's cached
// keys, so we can test which keys it asks for passwords for
repo, rec := newRepoToTestRepo(t, repo, false)
if serverManagesSnapshot {
err = repo.Initialize(rootPubKeyID, data.CanonicalSnapshotRole)
} else {
err = repo.Initialize(rootPubKeyID)
}
require.NoError(t, err, "error initializing repository")
// generates the target role, maybe the snapshot role
if serverManagesSnapshot {
rec.requireCreated(t, []string{data.CanonicalTargetsRole})
} else {
rec.requireCreated(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
}
// root is asked for signing the root role
rec.requireAsked(t, []string{data.CanonicalRootRole})
}
// TestInitRepoAttemptsExceeded tests error handling when passphrase.Retriever
// (or rather the user) insists on an incorrect password.
func TestInitRepoAttemptsExceeded(t *testing.T) {
testInitRepoAttemptsExceeded(t, data.ECDSAKey)
if !testing.Short() {
testInitRepoAttemptsExceeded(t, data.RSAKey)
}
}
func testInitRepoAttemptsExceeded(t *testing.T, rootType string) {
gun := "docker.com/notary"
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
defer os.RemoveAll(tempBaseDir)
ts, _, _ := simpleTestServer(t)
defer ts.Close()
retriever := passphrase.ConstantRetriever("password")
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType)
require.NoError(t, err, "error generating root key: %s", err)
retriever = passphrase.ConstantRetriever("incorrect password")
// repo.CryptoServices FileKeyStore caches the unlocked private key, so to test
// private key unlocking we need a new repo instance.
repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
err = repo.Initialize(rootPubKey.ID())
require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error())
}
// TestInitRepoPasswordInvalid tests error handling when passphrase.Retriever
// (or rather the user) fails to provide a correct password.
func TestInitRepoPasswordInvalid(t *testing.T) {
testInitRepoPasswordInvalid(t, data.ECDSAKey)
if !testing.Short() {
testInitRepoPasswordInvalid(t, data.RSAKey)
}
}
func giveUpPassphraseRetriever(_, _ string, _ bool, _ int) (string, bool, error) {
return "", true, nil
}
func testInitRepoPasswordInvalid(t *testing.T, rootType string) {
gun := "docker.com/notary"
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
defer os.RemoveAll(tempBaseDir)
ts, _, _ := simpleTestServer(t)
defer ts.Close()
retriever := passphrase.ConstantRetriever("password")
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType)
require.NoError(t, err, "error generating root key: %s", err)
// repo.CryptoServices FileKeyStore caches the unlocked private key, so to test
// private key unlocking we need a new repo instance.
repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
err = repo.Initialize(rootPubKey.ID())
require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error())
}
func addTarget(t *testing.T, repo *NotaryRepository, targetName, targetFile string,
roles ...string) *Target {
target, err := NewTarget(targetName, targetFile)
require.NoError(t, err, "error creating target")
err = repo.AddTarget(target, roles...)
require.NoError(t, err, "error adding target")
return target
}
// calls GetChangelist and gets the actual changes out
func getChanges(t *testing.T, repo *NotaryRepository) []changelist.Change {
changeList, err := repo.GetChangelist()
require.NoError(t, err)
return changeList.List()
}
// TestAddTargetToTargetRoleByDefault adds a target without specifying a role
// to a repo without delegations. Confirms that the changelist is created
// correctly, for the targets scope.
func TestAddTargetToTargetRoleByDefault(t *testing.T) {
testAddTargetToTargetRoleByDefault(t, false)
testAddTargetToTargetRoleByDefault(t, true)
}
func testAddTargetToTargetRoleByDefault(t *testing.T, clearCache bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
testAddOrDeleteTarget(t, repo, changelist.ActionCreate, nil,
[]string{data.CanonicalTargetsRole})
if clearCache {
// no key creation or signing happened, because add doesn't ever require signing
rec.requireCreated(t, nil)
rec.requireAsked(t, nil)
}
}
// Tests that adding a target to a repo or deleting a target from a repo,
// with the given roles, makes a change to the expected scopes
func testAddOrDeleteTarget(t *testing.T, repo *NotaryRepository, action string,
rolesToChange []string, expectedScopes []string) {
require.Len(t, getChanges(t, repo), 0, "should start with zero changes")
if action == changelist.ActionCreate {
// Add fixtures/intermediate-ca.crt as a target. There's no particular
// reason for using this file except that it happens to be available as
// a fixture.
addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt", rolesToChange...)
} else {
err := repo.RemoveTarget("latest", rolesToChange...)
require.NoError(t, err, "error removing target")
}
changes := getChanges(t, repo)
require.Len(t, changes, len(expectedScopes), "wrong number of changes files found")
foundScopes := make(map[string]bool)
for _, c := range changes { // there is only one
require.EqualValues(t, action, c.Action())
foundScopes[c.Scope()] = true
require.Equal(t, "target", c.Type())
require.Equal(t, "latest", c.Path())
if action == changelist.ActionCreate {
require.NotEmpty(t, c.Content())
} else {
require.Empty(t, c.Content())
}
}
require.Len(t, foundScopes, len(expectedScopes))
for _, expectedScope := range expectedScopes {
_, ok := foundScopes[expectedScope]
require.True(t, ok, "Target was not added/removed from %s", expectedScope)
}
// add/delete a second time
if action == changelist.ActionCreate {
addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt", rolesToChange...)
} else {
err := repo.RemoveTarget("current", rolesToChange...)
require.NoError(t, err, "error removing target")
}
changes = getChanges(t, repo)
require.Len(t, changes, 2*len(expectedScopes),
"wrong number of changelist files found")
newFileFound := false
foundScopes = make(map[string]bool)
for _, c := range changes {
if c.Path() != "latest" {
require.EqualValues(t, action, c.Action())
foundScopes[c.Scope()] = true
require.Equal(t, "target", c.Type())
require.Equal(t, "current", c.Path())
if action == changelist.ActionCreate {
require.NotEmpty(t, c.Content())
} else {
require.Empty(t, c.Content())
}
newFileFound = true
}
}
require.True(t, newFileFound, "second changelist file not found")
require.Len(t, foundScopes, len(expectedScopes))
for _, expectedScope := range expectedScopes {
_, ok := foundScopes[expectedScope]
require.True(t, ok, "Target was not added/removed from %s", expectedScope)
}
}
// TestAddTargetToSpecifiedValidRoles adds a target to the specified roles.
// Confirms that the changelist is created correctly, one for each of the
// the specified roles as scopes.
func TestAddTargetToSpecifiedValidRoles(t *testing.T) {
testAddTargetToSpecifiedValidRoles(t, false)
testAddTargetToSpecifiedValidRoles(t, true)
}
func testAddTargetToSpecifiedValidRoles(t *testing.T, clearCache bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
roleName := filepath.Join(data.CanonicalTargetsRole, "a")
testAddOrDeleteTarget(t, repo, changelist.ActionCreate,
[]string{
data.CanonicalTargetsRole,
roleName,
},
[]string{data.CanonicalTargetsRole, roleName})
if clearCache {
// no key creation or signing happened, because add doesn't ever require signing
rec.requireCreated(t, nil)
rec.requireAsked(t, nil)
}
}
// TestAddTargetToSpecifiedInvalidRoles expects errors to be returned if
// adding a target to an invalid role. If any of the roles are invalid,
// no targets are added to any roles.
func TestAddTargetToSpecifiedInvalidRoles(t *testing.T) {
testAddTargetToSpecifiedInvalidRoles(t, false)
testAddTargetToSpecifiedInvalidRoles(t, true)
}
func testAddTargetToSpecifiedInvalidRoles(t *testing.T, clearCache bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
invalidRoles := []string{
data.CanonicalRootRole,
data.CanonicalSnapshotRole,
data.CanonicalTimestampRole,
"target/otherrole",
"otherrole",
"TARGETS/ALLCAPSROLE",
}
for _, invalidRole := range invalidRoles {
target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt")
require.NoError(t, err, "error creating target")
err = repo.AddTarget(target, data.CanonicalTargetsRole, invalidRole)
require.Error(t, err, "Expected an ErrInvalidRole error")
require.IsType(t, data.ErrInvalidRole{}, err)
changes := getChanges(t, repo)
require.Len(t, changes, 0)
}
if clearCache {
// no key creation or signing happened, because add doesn't ever require signing
rec.requireCreated(t, nil)
rec.requireAsked(t, nil)
}
}
// General way to require that errors writing a changefile are propagated up
func testErrorWritingChangefiles(t *testing.T, writeChangeFile func(*NotaryRepository) error) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// first, make the actual changefile unwritable by making the changelist
// directory unwritable
changelistPath := filepath.Join(repo.tufRepoPath, "changelist")
err := os.MkdirAll(changelistPath, 0744)
require.NoError(t, err, "could not create changelist dir")
err = os.Chmod(changelistPath, 0600)
require.NoError(t, err, "could not change permission of changelist dir")
err = writeChangeFile(repo)
require.Error(t, err, "Expected an error writing the change")
require.IsType(t, &os.PathError{}, err)
// then break prevent the changlist directory from being able to be created
err = os.Chmod(changelistPath, 0744)
require.NoError(t, err, "could not change permission of temp dir")
err = os.RemoveAll(changelistPath)
require.NoError(t, err, "could not remove changelist dir")
// creating a changelist file so the directory can't be created
err = ioutil.WriteFile(changelistPath, []byte("hi"), 0644)
require.NoError(t, err, "could not write temporary file")
err = writeChangeFile(repo)
require.Error(t, err, "Expected an error writing the change")
require.IsType(t, &os.PathError{}, err)
}
// Ensures that AddTarget errors on invalid target input (no hashes)
func TestAddTargetWithInvalidTarget(t *testing.T) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt")
require.NoError(t, err, "error creating target")
// Clear the hashes
target.Hashes = data.Hashes{}
require.Error(t, repo.AddTarget(target, data.CanonicalTargetsRole))
}
// TestAddTargetErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestAddTargetErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
target, err := NewTarget("latest", "../fixtures/intermediate-ca.crt")
require.NoError(t, err, "error creating target")
return repo.AddTarget(target, data.CanonicalTargetsRole)
})
}
// TestRemoveTargetToTargetRoleByDefault removes a target without specifying a
// role from a repo. Confirms that the changelist is created correctly for
// the targets scope.
func TestRemoveTargetToTargetRoleByDefault(t *testing.T) {
testRemoveTargetToTargetRoleByDefault(t, false)
testRemoveTargetToTargetRoleByDefault(t, true)
}
func testRemoveTargetToTargetRoleByDefault(t *testing.T, clearCache bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
testAddOrDeleteTarget(t, repo, changelist.ActionDelete, nil,
[]string{data.CanonicalTargetsRole})
if clearCache {
// no key creation or signing happened, because remove doesn't ever require signing
rec.requireCreated(t, nil)
rec.requireAsked(t, nil)
}
}
// TestRemoveTargetFromSpecifiedValidRoles removes a target from the specified
// roles. Confirms that the changelist is created correctly, one for each of
// the the specified roles as scopes.
func TestRemoveTargetFromSpecifiedValidRoles(t *testing.T) {
testRemoveTargetFromSpecifiedValidRoles(t, false)
testRemoveTargetFromSpecifiedValidRoles(t, true)
}
func testRemoveTargetFromSpecifiedValidRoles(t *testing.T, clearCache bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
roleName := filepath.Join(data.CanonicalTargetsRole, "a")
testAddOrDeleteTarget(t, repo, changelist.ActionDelete,
[]string{
data.CanonicalTargetsRole,
roleName,
},
[]string{data.CanonicalTargetsRole, roleName})
if clearCache {
// no key creation or signing happened, because remove doesn't ever require signing
rec.requireCreated(t, nil)
rec.requireAsked(t, nil)
}
}
// TestRemoveTargetFromSpecifiedInvalidRoles expects errors to be returned if
// removing a target to an invalid role. If any of the roles are invalid,
// no targets are removed from any roles.
func TestRemoveTargetToSpecifiedInvalidRoles(t *testing.T) {
testRemoveTargetToSpecifiedInvalidRoles(t, false)
testRemoveTargetToSpecifiedInvalidRoles(t, true)
}
func testRemoveTargetToSpecifiedInvalidRoles(t *testing.T, clearCache bool) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
invalidRoles := []string{
data.CanonicalRootRole,
data.CanonicalSnapshotRole,
data.CanonicalTimestampRole,
"target/otherrole",
"otherrole",
}
for _, invalidRole := range invalidRoles {
err := repo.RemoveTarget("latest", data.CanonicalTargetsRole, invalidRole)
require.Error(t, err, "Expected an ErrInvalidRole error")
require.IsType(t, data.ErrInvalidRole{}, err)
changes := getChanges(t, repo)
require.Len(t, changes, 0)
}
if clearCache {
// no key creation or signing happened, because remove doesn't ever require signing
rec.requireCreated(t, nil)
rec.requireAsked(t, nil)
}
}
// TestRemoveTargetErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestRemoveTargetErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
return repo.RemoveTarget("latest", data.CanonicalTargetsRole)
})
}
// TestListTarget fakes serving signed metadata files over the test's
// internal HTTP server to ensure that ListTargets returns the correct number
// of listed targets.
// We test this with both an RSA and ECDSA root key
func TestListTarget(t *testing.T) {
testListEmptyTargets(t, data.ECDSAKey)
testListTarget(t, data.ECDSAKey)
testListTargetWithDelegates(t, data.ECDSAKey)
if !testing.Short() {
testListEmptyTargets(t, data.RSAKey)
testListTarget(t, data.RSAKey)
testListTargetWithDelegates(t, data.RSAKey)
}
}
func testListEmptyTargets(t *testing.T, rootType string) {
ts := fullTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
_, err := repo.ListTargets(data.CanonicalTargetsRole)
require.Error(t, err) // no trust data
}
// reads data from the repository in order to fake data being served via
// the ServeMux.
func fakeServerData(t *testing.T, repo *NotaryRepository, mux *http.ServeMux,
keys map[string]data.PrivateKey) {
timestampKey, ok := keys[data.CanonicalTimestampRole]
require.True(t, ok)
// Add timestamp key via the server's cryptoservice so it can sign
repo.CryptoService.AddKey(data.CanonicalTimestampRole, repo.gun, timestampKey)
savedTUFRepo := repo.tufRepo // in case this is overwritten
rootJSONFile := filepath.Join(repo.baseDir, "tuf",
filepath.FromSlash(repo.gun), "metadata", "root.json")
rootFileBytes, err := ioutil.ReadFile(rootJSONFile)
signedTargets, err := savedTUFRepo.SignTargets(
"targets", data.DefaultExpires("targets"))
require.NoError(t, err)
signedLevel1, err := savedTUFRepo.SignTargets(
"targets/level1",
data.DefaultExpires(data.CanonicalTargetsRole),
)
if _, ok := savedTUFRepo.Targets["targets/level1"]; ok {
require.NoError(t, err)
}
signedLevel2, err := savedTUFRepo.SignTargets(
"targets/level2",
data.DefaultExpires(data.CanonicalTargetsRole),
)
if _, ok := savedTUFRepo.Targets["targets/level2"]; ok {
require.NoError(t, err)
}
nested, err := savedTUFRepo.SignTargets(
"targets/level1/level2",
data.DefaultExpires(data.CanonicalTargetsRole),
)
if _, ok := savedTUFRepo.Targets["targets/level1/level2"]; ok {
require.NoError(t, err)
}
signedSnapshot, err := savedTUFRepo.SignSnapshot(
data.DefaultExpires("snapshot"))
require.NoError(t, err)
signedTimestamp, err := savedTUFRepo.SignTimestamp(
data.DefaultExpires("timestamp"))
require.NoError(t, err)
timestampJSON, _ := json.Marshal(signedTimestamp)
snapshotJSON, _ := json.Marshal(signedSnapshot)
targetsJSON, _ := json.Marshal(signedTargets)
level1JSON, _ := json.Marshal(signedLevel1)
level2JSON, _ := json.Marshal(signedLevel2)
nestedJSON, _ := json.Marshal(nested)
cksmBytes := sha256.Sum256(rootFileBytes)
rootChecksum := hex.EncodeToString(cksmBytes[:])
cksmBytes = sha256.Sum256(snapshotJSON)
snapshotChecksum := hex.EncodeToString(cksmBytes[:])
cksmBytes = sha256.Sum256(targetsJSON)
targetsChecksum := hex.EncodeToString(cksmBytes[:])
cksmBytes = sha256.Sum256(level1JSON)
level1Checksum := hex.EncodeToString(cksmBytes[:])
cksmBytes = sha256.Sum256(level2JSON)
level2Checksum := hex.EncodeToString(cksmBytes[:])
cksmBytes = sha256.Sum256(nestedJSON)
nestedChecksum := hex.EncodeToString(cksmBytes[:])
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root.json",
func(w http.ResponseWriter, r *http.Request) {
require.NoError(t, err)
fmt.Fprint(w, string(rootFileBytes))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/root."+rootChecksum+".json",
func(w http.ResponseWriter, r *http.Request) {
require.NoError(t, err)
fmt.Fprint(w, string(rootFileBytes))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/timestamp.json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(timestampJSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot.json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(snapshotJSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/snapshot."+snapshotChecksum+".json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(snapshotJSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets.json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(targetsJSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets."+targetsChecksum+".json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(targetsJSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1.json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(level1JSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1."+level1Checksum+".json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(level1JSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2.json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(level2JSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level2."+level2Checksum+".json",
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(level2JSON))
})
mux.HandleFunc("/v2/docker.com/notary/_trust/tuf/targets/level1/level2."+nestedChecksum+".json",
func(w http.ResponseWriter, r *http.Request) {
level2JSON, err := json.Marshal(nested)
require.NoError(t, err)
fmt.Fprint(w, string(level2JSON))
})
}
// We want to sort by name, so we can guarantee ordering.
type targetSorter []*TargetWithRole
func (k targetSorter) Len() int { return len(k) }
func (k targetSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
func (k targetSorter) Less(i, j int) bool { return k[i].Name < k[j].Name }
func testListTarget(t *testing.T, rootType string) {
ts, mux, keys := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// tests need to manually bootstrap timestamp as client doesn't generate it
err := repo.tufRepo.InitTimestamp()
require.NoError(t, err, "error creating repository: %s", err)
latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt")
// Apply the changelist. Normally, this would be done by Publish
// load the changelist for this repo
cl, err := changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
fakeServerData(t, repo, mux, keys)
targets, err := repo.ListTargets(data.CanonicalTargetsRole)
require.NoError(t, err)
// Should be two targets
require.Len(t, targets, 2, "unexpected number of targets returned by ListTargets")
sort.Stable(targetSorter(targets))
// the targets should both be found in the targets role
for _, foundTarget := range targets {
require.Equal(t, data.CanonicalTargetsRole, foundTarget.Role)
}
// current should be first
require.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match")
require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match")
// Also test GetTargetByName
newLatestTarget, err := repo.GetTargetByName("latest")
require.NoError(t, err)
require.Equal(t, data.CanonicalTargetsRole, newLatestTarget.Role)
require.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match")
newCurrentTarget, err := repo.GetTargetByName("current")
require.NoError(t, err)
require.Equal(t, data.CanonicalTargetsRole, newCurrentTarget.Role)
require.True(t, reflect.DeepEqual(*currentTarget, newCurrentTarget.Target), "current target does not match")
}
func testListTargetWithDelegates(t *testing.T, rootType string) {
ts, mux, keys := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// tests need to manually bootstrap timestamp as client doesn't generate it
err := repo.tufRepo.InitTimestamp()
require.NoError(t, err, "error creating repository: %s", err)
latestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
currentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt")
// setup delegated targets/level1 role
k, err := repo.CryptoService.Create("targets/level1", repo.gun, rootType)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level1", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false)
require.NoError(t, err)
delegatedTarget := addTarget(t, repo, "current", "../fixtures/root-ca.crt", "targets/level1")
otherTarget := addTarget(t, repo, "other", "../fixtures/root-ca.crt", "targets/level1")
// setup delegated targets/level2 role
k, err = repo.CryptoService.Create("targets/level2", repo.gun, rootType)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level2", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level2", []string{""}, []string{}, false)
require.NoError(t, err)
// this target should not show up as the one in targets/level1 takes higher priority
_ = addTarget(t, repo, "current", "../fixtures/notary-server.crt", "targets/level2")
// this target should show up as the name doesn't exist elsewhere
level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level2")
// Apply the changelist. Normally, this would be done by Publish
// load the changelist for this repo
cl, err := changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo, then clear it
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
require.NoError(t, cl.Clear(""))
_, ok := repo.tufRepo.Targets["targets/level1"].Signed.Targets["current"]
require.True(t, ok)
_, ok = repo.tufRepo.Targets["targets/level1"].Signed.Targets["other"]
require.True(t, ok)
_, ok = repo.tufRepo.Targets["targets/level2"].Signed.Targets["level2"]
require.True(t, ok)
// setup delegated targets/level1/level2 role separately, which can only modify paths prefixed with "level2"
// This is done separately due to target shadowing
k, err = repo.CryptoService.Create("targets/level1/level2", repo.gun, rootType)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{"level2"}, []string{}, false)
require.NoError(t, err)
nestedTarget := addTarget(t, repo, "level2", "../fixtures/notary-signer.crt", "targets/level1/level2")
// load the changelist for this repo
cl, err = changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
// check the changelist was applied
_, ok = repo.tufRepo.Targets["targets/level1/level2"].Signed.Targets["level2"]
require.True(t, ok)
fakeServerData(t, repo, mux, keys)
// test default listing
targets, err := repo.ListTargets()
require.NoError(t, err)
// Should be four targets
require.Len(t, targets, 4, "unexpected number of targets returned by ListTargets")
sort.Stable(targetSorter(targets))
// current should be first.
require.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match")
require.Equal(t, data.CanonicalTargetsRole, targets[0].Role)
require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match")
require.Equal(t, data.CanonicalTargetsRole, targets[1].Role)
// This target shadows the "level2" target in level1/level2
require.True(t, reflect.DeepEqual(*level2Target, targets[2].Target), "level2 target does not match")
require.Equal(t, "targets/level2", targets[2].Role)
require.True(t, reflect.DeepEqual(*otherTarget, targets[3].Target), "other target does not match")
require.Equal(t, "targets/level1", targets[3].Role)
// test listing with priority specified
targets, err = repo.ListTargets("targets/level1", data.CanonicalTargetsRole)
require.NoError(t, err)
// Should be four targets
require.Len(t, targets, 4, "unexpected number of targets returned by ListTargets")
sort.Stable(targetSorter(targets))
// current (in delegated role) should be first
require.True(t, reflect.DeepEqual(*delegatedTarget, targets[0].Target), "current target does not match")
require.Equal(t, "targets/level1", targets[0].Role)
require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match")
require.Equal(t, data.CanonicalTargetsRole, targets[1].Role)
// Now the level1/level2 target shadows the level2 target
require.True(t, reflect.DeepEqual(*nestedTarget, targets[2].Target), "level1/level2 target does not match")
require.Equal(t, "targets/level1/level2", targets[2].Role)
require.True(t, reflect.DeepEqual(*otherTarget, targets[3].Target), "other target does not match")
require.Equal(t, "targets/level1", targets[3].Role)
// Also test GetTargetByName
newLatestTarget, err := repo.GetTargetByName("latest")
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match")
require.Equal(t, data.CanonicalTargetsRole, newLatestTarget.Role)
newCurrentTarget, err := repo.GetTargetByName("current", "targets/level1", "targets")
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*delegatedTarget, newCurrentTarget.Target), "current target does not match")
require.Equal(t, "targets/level1", newCurrentTarget.Role)
newOtherTarget, err := repo.GetTargetByName("other")
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*otherTarget, newOtherTarget.Target), "other target does not match")
require.Equal(t, "targets/level1", newOtherTarget.Role)
newLevel2Target, err := repo.GetTargetByName("level2")
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*level2Target, newLevel2Target.Target), "level2 target does not match")
require.Equal(t, "targets/level2", newLevel2Target.Role)
// Shadow by prioritizing level1, but exclude level1/level2, so we should still get targets/level2's level2 target
newLevel2Target, err = repo.GetTargetByName("level2", "targets/level1", "targets/level2", "targets/level1/level2")
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*level2Target, newLevel2Target.Target), "level2 target does not match")
require.Equal(t, "targets/level2", newLevel2Target.Role)
// Prioritize level1 to get level1/level2's level2 target
newLevel2Target, err = repo.GetTargetByName("level2", "targets/level1")
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*nestedTarget, newLevel2Target.Target), "level2 target does not match")
require.Equal(t, "targets/level1/level2", newLevel2Target.Role)
}
func TestListTargetRestrictsDelegationPaths(t *testing.T) {
ts, mux, keys := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// tests need to manually bootstrap timestamp as client doesn't generate it
err := repo.tufRepo.InitTimestamp()
require.NoError(t, err, "error creating repository: %s", err)
// setup delegated targets/level1 role
k, err := repo.CryptoService.Create("targets/level1", repo.gun, data.ECDSAKey)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level1", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false)
require.NoError(t, err)
addTarget(t, repo, "level1-target", "../fixtures/root-ca.crt", "targets/level1")
addTarget(t, repo, "incorrectly-named-target", "../fixtures/root-ca.crt", "targets/level1")
// setup delegated targets/level2 role
err = repo.tufRepo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{""}, []string{}, false)
require.NoError(t, err)
addTarget(t, repo, "level2-target", "../fixtures/notary-server.crt", "targets/level1/level2")
addTarget(t, repo, "level1-level2-target", "../fixtures/notary-server.crt", "targets/level1/level2")
// Apply the changelist. Normally, this would be done by Publish
// load the changelist for this repo
cl, err := changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
require.NoError(t, cl.Clear(""))
// Now restrict the paths
err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{"level1"}, []string{}, false)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{"level1-level2", "level2"}, []string{}, false)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{}, []string{""}, false)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{}, []string{""}, false)
require.NoError(t, err)
// load the changelist for this repo
cl, err = changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
fakeServerData(t, repo, mux, keys)
// test default listing
targets, err := repo.ListTargets("targets/level1")
require.NoError(t, err)
// Should be four targets
require.Len(t, targets, 2, "unexpected number of targets returned by ListTargets")
sort.Stable(targetSorter(targets))
var foundLevel1, foundLevel2 bool
for _, tgts := range targets {
switch tgts.Name {
case "level1-target":
require.Equal(t, "targets/level1", tgts.Role)
foundLevel1 = true
case "level1-level2-target":
require.Equal(t, "targets/level1/level2", tgts.Role)
foundLevel2 = true
}
}
require.True(t, foundLevel1)
require.True(t, foundLevel2)
// test GetTargetByName
tgt, err := repo.GetTargetByName("level1-target", "targets/level1")
require.NoError(t, err)
require.NotNil(t, tgt)
require.Equal(t, tgt.Role, "targets/level1")
tgt, err = repo.GetTargetByName("level1-level2-target", "targets/level1")
require.NoError(t, err)
require.NotNil(t, tgt)
require.Equal(t, tgt.Role, "targets/level1/level2")
tgt, err = repo.GetTargetByName("level2-target", "targets/level1/level2")
require.Error(t, err)
require.Nil(t, tgt)
}
// TestValidateRootKey verifies that the public data in root.json for the root
// key is a valid x509 certificate.
func TestValidateRootKey(t *testing.T) {
testValidateRootKey(t, data.ECDSAKey)
if !testing.Short() {
testValidateRootKey(t, data.RSAKey)
}
}
func testValidateRootKey(t *testing.T, rootType string) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
rootJSONFile := filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun),
"metadata", "root.json")
jsonBytes, err := ioutil.ReadFile(rootJSONFile)
require.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err)
var decoded data.Signed
err = json.Unmarshal(jsonBytes, &decoded)
require.NoError(t, err, "error parsing TUF metadata file %s: %s", rootJSONFile, err)
var decodedRoot data.Root
err = json.Unmarshal(*decoded.Signed, &decodedRoot)
require.NoError(t, err, "error parsing root.json signed section: %s", err)
keyids := []string{}
for role, roleData := range decodedRoot.Roles {
if role == "root" {
keyids = append(keyids, roleData.KeyIDs...)
}
}
require.NotEmpty(t, keyids)
for _, keyid := range keyids {
key, ok := decodedRoot.Keys[keyid]
require.True(t, ok, "key id not found in keys")
_, err := trustmanager.LoadCertFromPEM(key.Public())
require.NoError(t, err, "key is not a valid cert")
}
}
// TestGetChangelist ensures that the changelist returned matches the changes
// added.
// We test this with both an RSA and ECDSA root key
func TestGetChangelist(t *testing.T) {
testGetChangelist(t, data.ECDSAKey)
if !testing.Short() {
testGetChangelist(t, data.RSAKey)
}
}
func testGetChangelist(t *testing.T, rootType string) {
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
require.Len(t, getChanges(t, repo), 0, "No changes should be in changelist yet")
// Create 2 targets
addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt")
// Test loading changelist
chgs := getChanges(t, repo)
require.Len(t, chgs, 2, "Wrong number of changes returned from changelist")
changes := make(map[string]changelist.Change)
for _, ch := range chgs {
changes[ch.Path()] = ch
}
currentChange := changes["current"]
require.NotNil(t, currentChange, "Expected changelist to contain a change for path 'current'")
require.EqualValues(t, changelist.ActionCreate, currentChange.Action())
require.Equal(t, "targets", currentChange.Scope())
require.Equal(t, "target", currentChange.Type())
require.Equal(t, "current", currentChange.Path())
latestChange := changes["latest"]
require.NotNil(t, latestChange, "Expected changelist to contain a change for path 'latest'")
require.EqualValues(t, changelist.ActionCreate, latestChange.Action())
require.Equal(t, "targets", latestChange.Scope())
require.Equal(t, "target", latestChange.Type())
require.Equal(t, "latest", latestChange.Path())
}
// Create a repo, instantiate a notary server, and publish the bare repo to the
// server, signing all the non-timestamp metadata. Root, targets, and snapshots
// (if locally signing) should be sent.
func TestPublishBareRepo(t *testing.T) {
testPublishNoData(t, data.ECDSAKey, false, true)
testPublishNoData(t, data.ECDSAKey, false, false)
testPublishNoData(t, data.ECDSAKey, true, true)
testPublishNoData(t, data.ECDSAKey, true, false)
if !testing.Short() {
testPublishNoData(t, data.RSAKey, false, true)
testPublishNoData(t, data.RSAKey, false, false)
testPublishNoData(t, data.RSAKey, true, true)
testPublishNoData(t, data.RSAKey, true, false)
}
}
func testPublishNoData(t *testing.T, rootType string, clearCache, serverManagesSnapshot bool) {
ts := fullTestServer(t)
defer ts.Close()
repo1, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL,
serverManagesSnapshot)
defer os.RemoveAll(repo1.baseDir)
var rec *passRoleRecorder
if clearCache {
rec = newRoleRecorder()
repo1, rec = newRepoToTestRepo(t, repo1, false)
}
require.NoError(t, repo1.Publish())
if clearCache {
// signing is only done by the target/snapshot keys
rec.requireCreated(t, nil)
if serverManagesSnapshot {
rec.requireAsked(t, []string{data.CanonicalTargetsRole})
} else {
rec.requireAsked(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
}
}
// use another repo to check metadata
repo2, _ := newRepoToTestRepo(t, repo1, true)
defer os.RemoveAll(repo2.baseDir)
targets, err := repo2.ListTargets()
require.NoError(t, err)
require.Empty(t, targets)
for _, role := range data.BaseRoles {
// we don't cache timstamp metadata
if role != data.CanonicalTimestampRole {
requireRepoHasExpectedMetadata(t, repo2, role, true)
}
}
}
// Publishing an uninitialized repo will fail, but initializing and republishing
// after should succeed
func TestPublishUninitializedRepo(t *testing.T) {
gun := "docker.com/notary"
ts := fullTestServer(t)
defer ts.Close()
// uninitialized repo should fail to publish
tempBaseDir, err := ioutil.TempDir("", "notary-tests")
require.NoError(t, err)
defer os.RemoveAll(tempBaseDir)
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL,
http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repository: %s", err)
err = repo.Publish()
require.Error(t, err)
// no metadata created
requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, false)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, false)
// now, initialize and republish in the same directory
rootPubKey, err := repo.CryptoService.Create("root", repo.gun, data.ECDSAKey)
require.NoError(t, err, "error generating root key: %s", err)
require.NoError(t, repo.Initialize(rootPubKey.ID()))
// now metadata is created
requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true)
require.NoError(t, repo.Publish())
}
// Create a repo, instantiate a notary server, and publish the repo with
// some targets to the server, signing all the non-timestamp metadata.
// We test this with both an RSA and ECDSA root key
func TestPublishClientHasSnapshotKey(t *testing.T) {
testPublishWithData(t, data.ECDSAKey, true, false)
testPublishWithData(t, data.ECDSAKey, false, false)
if !testing.Short() {
testPublishWithData(t, data.RSAKey, true, false)
testPublishWithData(t, data.RSAKey, false, false)
}
}
// Create a repo, instantiate a notary server (designating the server as the
// snapshot signer) , and publish the repo with some targets to the server,
// signing the root and targets metadata only. The server should sign just fine.
// We test this with both an RSA and ECDSA root key
func TestPublishAfterInitServerHasSnapshotKey(t *testing.T) {
testPublishWithData(t, data.ECDSAKey, true, true)
testPublishWithData(t, data.ECDSAKey, false, true)
if !testing.Short() {
testPublishWithData(t, data.RSAKey, true, true)
testPublishWithData(t, data.RSAKey, false, true)
}
}
func testPublishWithData(t *testing.T, rootType string, clearCache, serverManagesSnapshot bool) {
ts := fullTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL,
serverManagesSnapshot)
defer os.RemoveAll(repo.baseDir)
var rec *passRoleRecorder
if clearCache {
rec = newRoleRecorder()
repo, rec = newRepoToTestRepo(t, repo, false)
}
requirePublishToRolesSucceeds(t, repo, nil, []string{data.CanonicalTargetsRole})
if clearCache {
// signing is only done by the target/snapshot keys
rec.requireCreated(t, nil)
if serverManagesSnapshot {
rec.requireAsked(t, []string{data.CanonicalTargetsRole})
} else {
rec.requireAsked(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
}
}
}
// requires that adding to the given roles results in the targets actually being
// added only to the expected roles and no others
func requirePublishToRolesSucceeds(t *testing.T, repo1 *NotaryRepository,
publishToRoles []string, expectedPublishedRoles []string) {
// were there unpublished changes before?
changesOffset := len(getChanges(t, repo1))
// Create 2 targets - (actually 3, but we delete 1)
addTarget(t, repo1, "toDelete", "../fixtures/intermediate-ca.crt", publishToRoles...)
latestTarget := addTarget(
t, repo1, "latest", "../fixtures/intermediate-ca.crt", publishToRoles...)
currentTarget := addTarget(
t, repo1, "current", "../fixtures/intermediate-ca.crt", publishToRoles...)
repo1.RemoveTarget("toDelete", publishToRoles...)
// if no roles are provided, the default role is target
numRoles := int(math.Max(1, float64(len(publishToRoles))))
require.Len(t, getChanges(t, repo1), changesOffset+4*numRoles,
"wrong number of changelist files found")
// Now test Publish
err := repo1.Publish()
require.NoError(t, err)
require.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found")
// use another repo to check metadata
repo2, _ := newRepoToTestRepo(t, repo1, true)
defer os.RemoveAll(repo2.baseDir)
// Should be two targets per role
for _, role := range expectedPublishedRoles {
for _, repo := range []*NotaryRepository{repo1, repo2} {
targets, err := repo.ListTargets(role)
require.NoError(t, err)
require.Len(t, targets, 2,
"unexpected number of targets returned by ListTargets(%s)", role)
sort.Stable(targetSorter(targets))
require.True(t, reflect.DeepEqual(*currentTarget, targets[0].Target), "current target does not match")
require.Equal(t, role, targets[0].Role)
require.True(t, reflect.DeepEqual(*latestTarget, targets[1].Target), "latest target does not match")
require.Equal(t, role, targets[1].Role)
// Also test GetTargetByName
newLatestTarget, err := repo.GetTargetByName("latest", role)
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*latestTarget, newLatestTarget.Target), "latest target does not match")
require.Equal(t, role, newLatestTarget.Role)
newCurrentTarget, err := repo.GetTargetByName("current", role)
require.NoError(t, err)
require.True(t, reflect.DeepEqual(*currentTarget, newCurrentTarget.Target), "current target does not match")
require.Equal(t, role, newCurrentTarget.Role)
}
}
}
// After pulling a repo from the server, so there is a snapshots metadata file,
// push a different target to the server (the server is still the snapshot
// signer). The server should sign just fine.
// We test this with both an RSA and ECDSA root key
func TestPublishAfterPullServerHasSnapshotKey(t *testing.T) {
testPublishAfterPullServerHasSnapshotKey(t, data.ECDSAKey)
if !testing.Short() {
testPublishAfterPullServerHasSnapshotKey(t, data.RSAKey)
}
}
func testPublishAfterPullServerHasSnapshotKey(t *testing.T, rootType string) {
ts := fullTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, true)
defer os.RemoveAll(repo.baseDir)
// no timestamp metadata because that comes from the server
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false)
// no snapshot metadata because that comes from the server
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false)
// Publish something
published := addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt")
require.NoError(t, repo.Publish())
// still no timestamp or snapshot metadata info
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false)
// list, so that the snapshot metadata is pulled from server
targets, err := repo.ListTargets(data.CanonicalTargetsRole)
require.NoError(t, err)
require.Equal(t, []*TargetWithRole{{Target: *published, Role: data.CanonicalTargetsRole}}, targets)
// listing downloaded the timestamp and snapshot metadata info
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true)
// Publish again should succeed
addTarget(t, repo, "v2", "../fixtures/intermediate-ca.crt")
err = repo.Publish()
require.NoError(t, err)
}
// If neither the client nor the server has the snapshot key, signing will fail
// with an ErrNoKeys error.
// We test this with both an RSA and ECDSA root key
func TestPublishNoOneHasSnapshotKey(t *testing.T) {
testPublishNoOneHasSnapshotKey(t, data.ECDSAKey)
if !testing.Short() {
testPublishNoOneHasSnapshotKey(t, data.RSAKey)
}
}
func testPublishNoOneHasSnapshotKey(t *testing.T, rootType string) {
ts := fullTestServer(t)
defer ts.Close()
// create repo and delete the snapshot key and metadata
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
snapshotRole, ok := repo.tufRepo.Root.Signed.Roles[data.CanonicalSnapshotRole]
require.True(t, ok)
for _, keyID := range snapshotRole.KeyIDs {
repo.CryptoService.RemoveKey(keyID)
}
// ensure that the cryptoservice no longer has any snapshot keys
require.Len(t, repo.CryptoService.ListKeys(data.CanonicalSnapshotRole), 0)
// Publish something
addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt")
err := repo.Publish()
require.Error(t, err)
require.IsType(t, validation.ErrBadHierarchy{}, err)
}
// If the snapshot metadata is corrupt or the snapshot metadata is unreadable,
// we can't publish for the first time (whether the client or server has the
// snapshot key), because there is no existing data for us to download. If the
// repo has already been published, it doesn't matter if the metadata is corrupt
// because we can just redownload if it is.
func TestPublishSnapshotCorrupt(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// do not publish first - publish should fail with corrupt snapshot data even with server signing snapshot
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, true)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, false, false)
// do not publish first - publish should fail with corrupt snapshot data with local snapshot signing
repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, false, false)
// publish first - publish again should succeed despite corrupt snapshot data (server signing snapshot)
repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary3", ts.URL, true)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, true, true)
// publish first - publish again should succeed despite corrupt snapshot data (local snapshot signing)
repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary4", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalSnapshotRole, repo, true, true)
}
// If the targets metadata is corrupt or the targets metadata is unreadable,
// we can't publish for the first time, because there is no existing data for.
// us to download. If the repo has already been published, it doesn't matter
// if the metadata is corrupt because we can just redownload if it is.
func TestPublishTargetsCorrupt(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// do not publish first - publish should fail with corrupt snapshot data
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalTargetsRole, repo, false, false)
// publish first - publish again should succeed despite corrupt snapshot data
repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalTargetsRole, repo, true, true)
}
// If the root metadata is corrupt or the root metadata is unreadable,
// we can't publish for the first time. If there is already a remote root,
// we just download that and verify (using our trusted certificate trust
// anchors) that it is signed with the same keys, and if so, we just use the
// remote root.
func TestPublishRootCorrupt(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// do not publish first - publish should fail with corrupt snapshot data
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary1", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalRootRole, repo, false, false)
// publish first - publish should still fail if the local root is corrupt since
// we can't determine whether remote root is signed with the same key.
repo, _ = initializeRepo(t, data.ECDSAKey, "docker.com/notary2", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
testPublishBadMetadata(t, data.CanonicalRootRole, repo, true, false)
}
// When publishing snapshot, root, or target, if the repo hasn't been published
// before, if the metadata is corrupt, it can't be published.
func testPublishBadMetadata(t *testing.T, roleName string, repo *NotaryRepository,
publishFirst, succeeds bool) {
if publishFirst {
require.NoError(t, repo.Publish())
}
addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt")
// readable, but corrupt file
repo.fileStore.SetMeta(roleName, []byte("this isn't JSON"))
err := repo.Publish()
if succeeds {
require.NoError(t, err)
} else {
require.Error(t, err)
require.IsType(t, &json.SyntaxError{}, err)
}
// make an unreadable file by creating a directory instead of a file
path := fmt.Sprintf("%s.%s",
filepath.Join(repo.baseDir, tufDir, filepath.FromSlash(repo.gun),
"metadata", roleName), "json")
os.RemoveAll(path)
require.NoError(t, os.Mkdir(path, 0755))
defer os.RemoveAll(path)
err = repo.Publish()
if succeeds || publishFirst {
require.NoError(t, err)
} else {
require.Error(t, err)
require.IsType(t, &os.PathError{}, err)
}
}
// If the repo is not initialized, calling repo.Publish() should return ErrRepoNotInitialized
func TestNotInitializedOnPublish(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts := fullTestServer(t)
defer ts.Close()
repo, _, _ := createRepoAndKey(t, data.ECDSAKey, tempBaseDir, gun, ts.URL)
addTarget(t, repo, "v1", "../fixtures/intermediate-ca.crt")
err = repo.Publish()
require.Error(t, err)
require.IsType(t, ErrRepoNotInitialized{}, err)
}
type cannotCreateKeys struct {
signed.CryptoService
}
func (cs cannotCreateKeys) Create(_, _, _ string) (data.PublicKey, error) {
return nil, fmt.Errorf("Oh no I cannot create keys")
}
// If there is an error creating the local keys, no call is made to get a
// remote key.
func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
defer os.RemoveAll(tempBaseDir)
require.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
requestMade := false
ts := httptest.NewServer(http.HandlerFunc(
func(http.ResponseWriter, *http.Request) { requestMade = true }))
defer ts.Close()
repo, err := NewNotaryRepository(
tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever))
rootPubKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey)
require.NoError(t, err, "error generating root key: %s", err)
repo.CryptoService = cannotCreateKeys{CryptoService: cs}
err = repo.Initialize(rootPubKey.ID(), data.CanonicalSnapshotRole)
require.Error(t, err)
require.Contains(t, err.Error(), "Oh no I cannot create keys")
require.False(t, requestMade)
}
func createKey(t *testing.T, repo *NotaryRepository, role string, x509 bool) data.PublicKey {
key, err := repo.CryptoService.Create(role, repo.gun, data.ECDSAKey)
require.NoError(t, err, "error creating key")
if x509 {
start := time.Now().AddDate(0, 0, -1)
privKey, _, err := repo.CryptoService.GetPrivateKey(key.ID())
require.NoError(t, err)
cert, err := cryptoservice.GenerateCertificate(
privKey, role, start, start.AddDate(1, 0, 0),
)
require.NoError(t, err)
return data.NewECDSAx509PublicKey(trustmanager.CertToPEM(cert))
}
return key
}
// Publishing delegations works so long as the delegation parent exists by the
// time that delegation addition change is applied. Most of the tests for
// applying delegation changes in in helpers_test.go (applyTargets tests), so
// this is just a sanity test to make sure Publish calls it correctly
func TestPublishDelegations(t *testing.T) {
testPublishDelegations(t, true, false)
testPublishDelegations(t, false, false)
}
func TestPublishDelegationsX509(t *testing.T) {
testPublishDelegations(t, true, true)
testPublishDelegations(t, false, true)
}
func testPublishDelegations(t *testing.T, clearCache, x509Keys bool) {
ts := fullTestServer(t)
defer ts.Close()
repo1, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo1.baseDir)
delgKey := createKey(t, repo1, "targets/a", x509Keys)
// This should publish fine, even though targets/a/b is dependent upon
// targets/a, because these should execute in order
for _, delgName := range []string{"targets/a", "targets/a/b", "targets/c"} {
require.NoError(t,
repo1.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}),
"error creating delegation")
}
require.Len(t, getChanges(t, repo1), 6, "wrong number of changelist files found")
var rec *passRoleRecorder
if clearCache {
repo1, rec = newRepoToTestRepo(t, repo1, false)
}
require.NoError(t, repo1.Publish())
require.Len(t, getChanges(t, repo1), 0, "wrong number of changelist files found")
if clearCache {
// when publishing, only the parents of the delegations created need to be signed
// (and snapshot)
rec.requireAsked(t, []string{data.CanonicalTargetsRole, "targets/a", data.CanonicalSnapshotRole})
rec.clear()
}
// this should not publish, because targets/z doesn't exist
require.NoError(t,
repo1.AddDelegation("targets/z/y", []data.PublicKey{delgKey}, []string{""}),
"error creating delegation")
require.Len(t, getChanges(t, repo1), 2, "wrong number of changelist files found")
require.Error(t, repo1.Publish())
require.Len(t, getChanges(t, repo1), 2, "wrong number of changelist files found")
if clearCache {
rec.requireAsked(t, nil)
}
// use another repo to check metadata
repo2, _ := newRepoToTestRepo(t, repo1, false)
defer os.RemoveAll(repo2.baseDir)
// pull
_, err := repo2.ListTargets()
require.NoError(t, err, "unable to pull repo")
for _, repo := range []*NotaryRepository{repo1, repo2} {
// targets should have delegations targets/a and targets/c
targets := repo.tufRepo.Targets[data.CanonicalTargetsRole]
require.Len(t, targets.Signed.Delegations.Roles, 2)
require.Len(t, targets.Signed.Delegations.Keys, 1)
_, ok := targets.Signed.Delegations.Keys[delgKey.ID()]
require.True(t, ok)
foundRoleNames := make(map[string]bool)
for _, r := range targets.Signed.Delegations.Roles {
foundRoleNames[r.Name] = true
}
require.True(t, foundRoleNames["targets/a"])
require.True(t, foundRoleNames["targets/c"])
// targets/a should have delegation targets/a/b only
a := repo.tufRepo.Targets["targets/a"]
require.Len(t, a.Signed.Delegations.Roles, 1)
require.Len(t, a.Signed.Delegations.Keys, 1)
_, ok = a.Signed.Delegations.Keys[delgKey.ID()]
require.True(t, ok)
require.Equal(t, "targets/a/b", a.Signed.Delegations.Roles[0].Name)
}
}
// If a changelist specifies a particular role to push targets to, and there
// is a role but no key, publish should just fail outright.
func TestPublishTargetsDelegationScopeFailIfNoKeys(t *testing.T) {
testPublishTargetsDelegationScopeFailIfNoKeys(t, true)
testPublishTargetsDelegationScopeFailIfNoKeys(t, false)
}
func testPublishTargetsDelegationScopeFailIfNoKeys(t *testing.T, clearCache bool) {
ts := fullTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// generate a key that isn't in the cryptoservice, so we can't sign this
// one
aPrivKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
require.NoError(t, err, "error generating key that is not in our cryptoservice")
aPubKey := data.PublicKeyFromPrivate(aPrivKey)
var rec *passRoleRecorder
if clearCache {
repo, rec = newRepoToTestRepo(t, repo, false)
}
// ensure that the role exists
require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{aPubKey}, []string{""}))
require.NoError(t, repo.Publish())
if clearCache {
rec.requireAsked(t, []string{data.CanonicalTargetsRole, data.CanonicalSnapshotRole})
rec.clear()
}
// add a target to targets/a/b - no role b, so it falls back on a, which
// exists but there is no signing key for
addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt", "targets/a/b")
require.Len(t, getChanges(t, repo), 1, "wrong number of changelist files found")
// Now Publish should fail
require.Error(t, repo.Publish())
require.Len(t, getChanges(t, repo), 1, "wrong number of changelist files found")
if clearCache {
rec.requireAsked(t, nil)
rec.clear()
}
targets, err := repo.ListTargets("targets", "targets/a", "targets/a/b")
require.NoError(t, err)
require.Empty(t, targets)
}
// If a changelist specifies a particular role to push targets to, and such
// a role and the keys are present, publish will write to that role only, and
// not its parents. This tests the case where the local machine knows about
// all the roles (in fact, the role creations will be applied before the
// targets)
func TestPublishTargetsDelegationSuccessLocallyHasRoles(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)
for _, delgName := range []string{"targets/a", "targets/a/b"} {
delgKey := createKey(t, repo, delgName, false)
require.NoError(t,
repo.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}),
"error creating delegation")
}
// just always check signing now, we've already established we can publish
// delegations with and without the metadata and key cache
var rec *passRoleRecorder
repo, rec = newRepoToTestRepo(t, repo, false)
requirePublishToRolesSucceeds(t, repo, []string{"targets/a/b"},
[]string{"targets/a/b"})
// first time publishing, so everything gets signed
rec.requireAsked(t, []string{data.CanonicalTargetsRole, "targets/a", "targets/a/b",
data.CanonicalSnapshotRole})
}
// If a changelist specifies a particular role to push targets to, and the role
// is present, publish will write to that role only. The targets keys are not
// needed.
func TestPublishTargetsDelegationNoTargetsKeyNeeded(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)
for _, delgName := range []string{"targets/a", "targets/a/b"} {
delgKey := createKey(t, repo, delgName, false)
require.NoError(t,
repo.AddDelegation(delgName, []data.PublicKey{delgKey}, []string{""}),
"error creating delegation")
}
// just always check signing now, we've already established we can publish
// delegations with and without the metadata and key cache
var rec *passRoleRecorder
repo, rec = newRepoToTestRepo(t, repo, false)
require.NoError(t, repo.Publish())
// first time publishing, so all delegation parents get signed
rec.requireAsked(t, []string{data.CanonicalTargetsRole, "targets/a", data.CanonicalSnapshotRole})
rec.clear()
// remove targets key - it is not even needed
targetsKeys := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
require.Len(t, targetsKeys, 1)
require.NoError(t, repo.CryptoService.RemoveKey(targetsKeys[0]))
requirePublishToRolesSucceeds(t, repo, []string{"targets/a/b"},
[]string{"targets/a/b"})
// only the target delegation gets signed - snapshot key has already been cached
rec.requireAsked(t, []string{"targets/a/b"})
}
// If a changelist specifies a particular role to push targets to, and is such
// a role and the keys are present, publish will write to that role only, and
// not its parents. Tests:
// - case where the local doesn't know about all the roles, and has to download
// them before publish.
// - owner of a repo may not have the delegated keys, so can't sign a delegated
// role
func TestPublishTargetsDelegationSuccessNeedsToDownloadRoles(t *testing.T) {
gun := "docker.com/notary"
ts := fullTestServer(t)
defer ts.Close()
// this is the original repo - it owns the root/targets keys and creates
// the delegation to which it doesn't have the key (so server snapshot
// signing would be required)
ownerRepo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true)
defer os.RemoveAll(ownerRepo.baseDir)
// this is a user, or otherwise a repo that only has access to the delegation
// key so it can publish targets to the delegated role
delgRepo, _ := newRepoToTestRepo(t, ownerRepo, true)
defer os.RemoveAll(delgRepo.baseDir)
// create a key on the owner repo
aKey, err := ownerRepo.CryptoService.Create("targets/a", gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
// create a key on the delegated repo
bKey, err := delgRepo.CryptoService.Create("targets/a/b", gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
// clear metadata and unencrypted private key cache
var ownerRec, delgRec *passRoleRecorder
ownerRepo, ownerRec = newRepoToTestRepo(t, ownerRepo, false)
delgRepo, delgRec = newRepoToTestRepo(t, delgRepo, false)
// owner creates delegations, adds the delegated key to them, and publishes them
require.NoError(t,
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
"error creating delegation")
require.NoError(t,
ownerRepo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}),
"error creating delegation")
require.NoError(t, ownerRepo.Publish())
// delegation parents all get signed
ownerRec.requireAsked(t, []string{data.CanonicalTargetsRole, "targets/a"})
// delegated repo now publishes to delegated roles, but it will need
// to download those roles first, since it doesn't know about them
requirePublishToRolesSucceeds(t, delgRepo, []string{"targets/a/b"},
[]string{"targets/a/b"})
delgRec.requireAsked(t, []string{"targets/a/b"})
}
// Ensure that two clients can publish delegations with two different keys and
// the changes will not clobber each other.
func TestPublishTargetsDelegationFromTwoRepos(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// this happens to be the client that creates the repo, but can also
// write a delegation
repo1, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true)
defer os.RemoveAll(repo1.baseDir)
// this is the second writable repo
repo2, _ := newRepoToTestRepo(t, repo1, true)
defer os.RemoveAll(repo2.baseDir)
// create keys for each repo
key1, err := repo1.CryptoService.Create("targets/a", repo1.gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
// create a key on the delegated repo
key2, err := repo2.CryptoService.Create("targets/a", repo2.gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
// delegation includes both keys
require.NoError(t,
repo1.AddDelegation("targets/a", []data.PublicKey{key1, key2}, []string{""}),
"error creating delegation")
require.NoError(t, repo1.Publish())
// clear metadata and unencrypted private key cache
var rec1, rec2 *passRoleRecorder
repo1, rec1 = newRepoToTestRepo(t, repo1, false)
repo2, rec2 = newRepoToTestRepo(t, repo2, false)
// both repos add targets and publish
addTarget(t, repo1, "first", "../fixtures/root-ca.crt", "targets/a")
require.NoError(t, repo1.Publish())
rec1.requireAsked(t, []string{"targets/a"})
rec1.clear()
addTarget(t, repo2, "second", "../fixtures/root-ca.crt", "targets/a")
require.NoError(t, repo2.Publish())
rec2.requireAsked(t, []string{"targets/a"})
rec2.clear()
// first repo can publish again
addTarget(t, repo1, "third", "../fixtures/root-ca.crt", "targets/a")
require.NoError(t, repo1.Publish())
// key has been cached now
rec1.requireAsked(t, nil)
rec1.clear()
// both repos should be able to see all targets
for _, repo := range []*NotaryRepository{repo1, repo2} {
targets, err := repo.ListTargets()
require.NoError(t, err)
require.Len(t, targets, 3)
found := make(map[string]bool)
for _, t := range targets {
found[t.Name] = true
}
for _, targetName := range []string{"first", "second", "third"} {
_, ok := found[targetName]
require.True(t, ok)
}
}
}
// A client who could publish before can no longer publish once the owner
// removes their delegation key from the delegation role.
func TestPublishRemoveDelegationKeyFromDelegationRole(t *testing.T) {
gun := "docker.com/notary"
ts := fullTestServer(t)
defer ts.Close()
// this is the original repo - it owns the root/targets keys and creates
// the delegation to which it doesn't have the key (so server snapshot
// signing would be required)
ownerRepo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true)
defer os.RemoveAll(ownerRepo.baseDir)
// this is a user, or otherwise a repo that only has access to the delegation
// key so it can publish targets to the delegated role
delgRepo, _ := newRepoToTestRepo(t, ownerRepo, true)
defer os.RemoveAll(delgRepo.baseDir)
// create a key on the delegated repo
aKey, err := delgRepo.CryptoService.Create("targets/a", delgRepo.gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
// owner creates delegation, adds the delegated key to it, and publishes it
require.NoError(t,
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
"error creating delegation")
require.NoError(t, ownerRepo.Publish())
// delegated repo can now publish to delegated role
addTarget(t, delgRepo, "v1", "../fixtures/root-ca.crt", "targets/a")
require.NoError(t, delgRepo.Publish())
// owner revokes delegation
// note there is no removekeyfromdelegation yet, so here's a hack to do so
newKey, err := ownerRepo.CryptoService.Create("targets/a", ownerRepo.gun, data.ECDSAKey)
require.NoError(t, err)
tdJSON, err := json.Marshal(&changelist.TufDelegation{
NewThreshold: 1,
AddKeys: data.KeyList([]data.PublicKey{newKey}),
RemoveKeys: []string{aKey.ID()},
})
require.NoError(t, err)
cl, err := changelist.NewFileChangelist(filepath.Join(ownerRepo.tufRepoPath, "changelist"))
require.NoError(t, cl.Add(changelist.NewTufChange(
changelist.ActionUpdate,
"targets/a",
changelist.TypeTargetsDelegation,
"",
tdJSON,
)))
cl.Close()
require.NoError(t, ownerRepo.Publish())
// delegated repo can now no longer publish to delegated role
addTarget(t, delgRepo, "v2", "../fixtures/root-ca.crt", "targets/a")
require.Error(t, delgRepo.Publish())
}
// A client who could publish before can no longer publish once the owner
// deletes the delegation
func TestPublishRemoveDelegation(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// this is the original repo - it owns the root/targets keys and creates
// the delegation to which it doesn't have the key (so server snapshot
// signing would be required)
ownerRepo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true)
defer os.RemoveAll(ownerRepo.baseDir)
// this is a user, or otherwise a repo that only has access to the delegation
// key so it can publish targets to the delegated role
delgRepo, _ := newRepoToTestRepo(t, ownerRepo, true)
defer os.RemoveAll(delgRepo.baseDir)
// create a key on the delegated repo
aKey, err := delgRepo.CryptoService.Create("targets/a", delgRepo.gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
// owner creates delegation, adds the delegated key to it, and publishes it
require.NoError(t,
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
"error creating delegation")
require.NoError(t, ownerRepo.Publish())
// delegated repo can now publish to delegated role
addTarget(t, delgRepo, "v1", "../fixtures/root-ca.crt", "targets/a")
require.NoError(t, delgRepo.Publish())
// owner removes delegation
aKeyCanonicalID, err := utils.CanonicalKeyID(aKey)
require.NoError(t, err)
require.NoError(t, ownerRepo.RemoveDelegationKeys("targets/a", []string{aKeyCanonicalID}))
require.NoError(t, ownerRepo.Publish())
// delegated repo can now no longer publish to delegated role
addTarget(t, delgRepo, "v2", "../fixtures/root-ca.crt", "targets/a")
require.Error(t, delgRepo.Publish())
}
// If the delegation data is corrupt or unreadable, it doesn't matter because
// all the delegation information is just re-downloaded. When bootstrapping
// the repository from disk, we just don't load the data from disk because
// there should not be anything there yet.
func TestPublishSucceedsDespiteDelegationCorrupt(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)
delgKey, err := repo.CryptoService.Create("targets/a", repo.gun, data.ECDSAKey)
require.NoError(t, err, "error creating delegation key")
require.NoError(t,
repo.AddDelegation("targets/a", []data.PublicKey{delgKey}, []string{""}),
"error creating delegation")
testPublishBadMetadata(t, "targets/a", repo, false, true)
// publish again, now that it has already been published, and again there
// is no error.
testPublishBadMetadata(t, "targets/a", repo, true, true)
}
// Rotate invalid roles, or attempt to delegate target signing to the server
func TestRotateKeyInvalidRole(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)
// create a delegation
pubKey, err := repo.CryptoService.Create("targets/releases", "docker.com/notary", data.ECDSAKey)
require.NoError(t, err)
require.NoError(t, repo.AddDelegation("targets/releases", []data.PublicKey{pubKey}, []string{""}))
require.NoError(t, repo.Publish())
require.NoError(t, repo.Update(false))
// rotating a root key to the server fails
require.Error(t, repo.RotateKey(data.CanonicalRootRole, true),
"Rotating a root key with server-managing the key should fail")
// rotating a targets key to the server fails
require.Error(t, repo.RotateKey(data.CanonicalTargetsRole, true),
"Rotating a targets key with server-managing the key should fail")
// rotating a timestamp key locally fails
require.Error(t, repo.RotateKey(data.CanonicalTimestampRole, false),
"Rotating a timestamp key locally should fail")
// rotating a delegation key fails
require.Error(t, repo.RotateKey("targets/releases", false),
"Rotating a delegation key should fail")
// rotating a not a real role key fails
require.Error(t, repo.RotateKey("nope", false),
"Rotating a non-real role key should fail")
}
// If remotely rotating key fails, the failure is propagated
func TestRemoteRotationError(t *testing.T) {
ts, _, _ := simpleTestServer(t)
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true)
defer os.RemoveAll(repo.baseDir)
ts.Close()
// server has died, so this should fail
err := repo.RotateKey(data.CanonicalTimestampRole, true)
require.Error(t, err)
require.Contains(t, err.Error(), "unable to rotate remote key")
}
// Rotates the keys. After the rotation, downloading the latest metadata
// and require that the keys have changed
func requireRotationSuccessful(t *testing.T, repo1 *NotaryRepository, keysToRotate map[string]bool) {
// Create a new repo that is used to download the data after the rotation
repo2, _ := newRepoToTestRepo(t, repo1, true)
defer os.RemoveAll(repo2.baseDir)
repos := []*NotaryRepository{repo1, repo2}
oldRoles := make(map[string]data.BaseRole)
for roleName := range keysToRotate {
baseRole, err := repo1.tufRepo.GetBaseRole(roleName)
require.NoError(t, err)
require.Len(t, baseRole.Keys, 1)
oldRoles[roleName] = baseRole
}
// Confirm no changelists get published
changesPre := getChanges(t, repo1)
// Do rotation
for role, serverManaged := range keysToRotate {
require.NoError(t, repo1.RotateKey(role, serverManaged))
}
changesPost := getChanges(t, repo1)
require.Equal(t, changesPre, changesPost)
// Download data from remote and check that keys have changed
for _, repo := range repos {
err := repo.Update(true)
require.NoError(t, err)
for roleName, isRemoteKey := range keysToRotate {
baseRole, err := repo1.tufRepo.GetBaseRole(roleName)
require.NoError(t, err)
require.Len(t, baseRole.Keys, 1)
// in the new key is not the same as any of the old keys
for oldKeyID, oldPubKey := range oldRoles[roleName].Keys {
_, ok := baseRole.Keys[oldKeyID]
require.False(t, ok)
// in the old repo, the old keys have been removed not just from
// the TUF file, but from the cryptoservice (unless it's a root
// key, in which case it should NOT be removed)
if repo == repo1 {
canonicalID, err := utils.CanonicalKeyID(oldPubKey)
require.NoError(t, err)
_, _, err = repo.CryptoService.GetPrivateKey(canonicalID)
switch roleName {
case data.CanonicalRootRole:
require.NoError(t, err)
default:
require.Error(t, err)
}
}
}
// On the old repo, the new key is present in the cryptoservice, or
// not present if remote.
if repo == repo1 {
pubKey := baseRole.ListKeys()[0]
canonicalID, err := utils.CanonicalKeyID(pubKey)
require.NoError(t, err)
key, _, err := repo.CryptoService.GetPrivateKey(canonicalID)
if isRemoteKey {
require.Error(t, err)
require.Nil(t, key)
} else {
require.NoError(t, err)
require.NotNil(t, key)
}
}
}
}
}
// Initialize repo to have the server sign snapshots (remote snapshot key)
// Without downloading a server-signed snapshot file, rotate keys so that
// snapshots are locally signed (local snapshot key)
// Assert that we can publish.
func TestRotateBeforePublishFromRemoteKeyToLocalKey(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, true)
defer os.RemoveAll(repo.baseDir)
// Adding a target will allow us to confirm the repository is still valid
// after rotating the keys when we publish (and that rotation doesn't publish
// non-key-rotation changes)
addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
requireRotationSuccessful(t, repo, map[string]bool{
data.CanonicalRootRole: false,
data.CanonicalTargetsRole: false,
data.CanonicalSnapshotRole: false})
require.NoError(t, repo.Publish())
_, err := repo.GetTargetByName("latest")
require.NoError(t, err)
}
// Initialize a repo, locally signed snapshots
// Publish some content (so that the server has a root.json), and download root.json
// Rotate keys
// Download the latest metadata and require that the keys have changed.
func TestRotateKeyAfterPublishNoServerManagementChange(t *testing.T) {
testRotateKeySuccess(t, false, map[string]bool{data.CanonicalRootRole: false})
testRotateKeySuccess(t, false, map[string]bool{data.CanonicalTargetsRole: false})
testRotateKeySuccess(t, false, map[string]bool{data.CanonicalSnapshotRole: false})
// rotate multiple keys at once before publishing
testRotateKeySuccess(t, false, map[string]bool{
data.CanonicalRootRole: false,
data.CanonicalSnapshotRole: false,
data.CanonicalTargetsRole: false})
}
// Tests rotating keys when there's a change from locally managed keys to
// remotely managed keys and vice versa
// Before rotating, publish some content (so that the server has a root.json),
// and download root.json
func TestRotateKeyAfterPublishServerManagementChange(t *testing.T) {
// delegate snapshot key management to the server
testRotateKeySuccess(t, false, map[string]bool{
data.CanonicalSnapshotRole: true,
data.CanonicalTargetsRole: false,
data.CanonicalRootRole: false,
})
// reclaim snapshot key management from the server
testRotateKeySuccess(t, true, map[string]bool{
data.CanonicalSnapshotRole: false,
data.CanonicalTargetsRole: false,
data.CanonicalRootRole: false,
})
}
func testRotateKeySuccess(t *testing.T, serverManagesSnapshotInit bool,
keysToRotate map[string]bool) {
ts := fullTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL,
serverManagesSnapshotInit)
defer os.RemoveAll(repo.baseDir)
// Adding a target will allow us to confirm the repository is still valid after
// rotating the keys.
addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
requireRotationSuccessful(t, repo, keysToRotate)
// Publish
require.NoError(t, repo.Publish())
// Get root.json and capture targets + snapshot key IDs
_, err := repo.GetTargetByName("latest")
require.NoError(t, err)
var keysToExpectCreated []string
for role, serverManaged := range keysToRotate {
if !serverManaged {
keysToExpectCreated = append(keysToExpectCreated, role)
}
}
}
func logRepoTrustRoot(t *testing.T, prefix string, repo *NotaryRepository) {
logrus.Debugf("==== %s", prefix)
root := repo.tufRepo.Root
logrus.Debugf("Root signatures:")
for _, s := range root.Signatures {
logrus.Debugf("\t%s", s.KeyID)
}
logrus.Debugf("Valid root keys:")
for _, k := range root.Signed.Roles[data.CanonicalRootRole].KeyIDs {
logrus.Debugf("\t%s", k)
}
}
// ID of the (only) certificate trusted by the root role metadata
func rootRoleCertID(t *testing.T, repo *NotaryRepository) string {
rootKeys := repo.tufRepo.Root.Signed.Roles[data.CanonicalRootRole].KeyIDs
require.Len(t, rootKeys, 1)
return rootKeys[0]
}
func TestRotateRootKey(t *testing.T) {
ts := fullTestServer(t)
defer ts.Close()
// Set up author's view of the repo and publish first version.
authorRepo, _ := initializeRepo(t, data.ECDSAKey, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(authorRepo.baseDir)
err := authorRepo.Publish()
require.NoError(t, err)
oldRootCertID := rootRoleCertID(t, authorRepo)
oldRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole)
require.NoError(t, err)
oldCanonicalKeyID, err := utils.CanonicalKeyID(oldRootRole.Keys[oldRootCertID])
require.NoError(t, err)
// Initialize an user, using the original root cert and key.
userRepo, _ := newRepoToTestRepo(t, authorRepo, true)
defer os.RemoveAll(userRepo.baseDir)
err = userRepo.Update(false)
require.NoError(t, err)
// Rotate root certificate and key.
logRepoTrustRoot(t, "original", authorRepo)
err = authorRepo.RotateKey(data.CanonicalRootRole, false)
require.NoError(t, err)
logRepoTrustRoot(t, "post-rotate", authorRepo)
require.NoError(t, authorRepo.Update(false))
newRootRole, err := authorRepo.tufRepo.GetBaseRole(data.CanonicalRootRole)
require.False(t, newRootRole.Equals(oldRootRole))
// not only is the root cert different, but the private key is too
newRootCertID := rootRoleCertID(t, authorRepo)
require.NotEqual(t, oldRootCertID, newRootCertID)
newCanonicalKeyID, err := utils.CanonicalKeyID(newRootRole.Keys[newRootCertID])
require.NoError(t, err)
require.NotEqual(t, oldCanonicalKeyID, newCanonicalKeyID)
// Set up a target to verify the repo is actually usable.
_, err = userRepo.GetTargetByName("current")
require.Error(t, err)
addTarget(t, authorRepo, "current", "../fixtures/intermediate-ca.crt")
// Publish the target, which does an update and pulls down the latest metadata, and
// should update the trusted root
logRepoTrustRoot(t, "pre-publish", authorRepo)
err = authorRepo.Publish()
require.NoError(t, err)
logRepoTrustRoot(t, "post-publish", authorRepo)
// Verify the user can use the rotated repo, and see the added target.
_, err = userRepo.GetTargetByName("current")
require.NoError(t, err)
logRepoTrustRoot(t, "client", userRepo)
// Verify that clients initialized post-rotation can use the repo, and use
// the new certificate immediately.
freshUserRepo, _ := newRepoToTestRepo(t, authorRepo, true)
defer os.RemoveAll(freshUserRepo.baseDir)
_, err = freshUserRepo.GetTargetByName("current")
require.NoError(t, err)
require.Equal(t, newRootCertID, rootRoleCertID(t, freshUserRepo))
logRepoTrustRoot(t, "fresh client", freshUserRepo)
// Verify that the user initialized with the original certificate eventually
// rotates to the new certificate.
err = userRepo.Update(false)
require.NoError(t, err)
logRepoTrustRoot(t, "user refresh 1", userRepo)
require.Equal(t, newRootCertID, rootRoleCertID(t, userRepo))
}
// If there is no local cache, notary operations return the remote error code
func TestRemoteServerUnavailableNoLocalCache(t *testing.T) {
tempBaseDir, err := ioutil.TempDir("/tmp", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
defer os.RemoveAll(tempBaseDir)
ts := errorTestServer(t, 500)
defer ts.Close()
repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary",
ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{})
require.NoError(t, err, "error creating repo: %s", err)
_, err = repo.ListTargets(data.CanonicalTargetsRole)
require.Error(t, err)
require.IsType(t, store.ErrServerUnavailable{}, err)
_, err = repo.GetTargetByName("targetName")
require.Error(t, err)
require.IsType(t, store.ErrServerUnavailable{}, err)
err = repo.Publish()
require.Error(t, err)
require.IsType(t, store.ErrServerUnavailable{}, err)
}
// AddDelegation creates a valid changefile (rejects invalid delegation names,
// but does not check the delegation hierarchy). When applied, the change adds
// a new delegation role with the correct keys.
func TestAddDelegationChangefileValid(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
require.NotEmpty(t, targetKeyIds)
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
require.NotNil(t, targetPubKey)
err := repo.AddDelegation("root", []data.PublicKey{targetPubKey}, []string{""})
require.Error(t, err)
require.IsType(t, data.ErrInvalidRole{}, err)
require.Empty(t, getChanges(t, repo))
// to show that adding does not care about the hierarchy
err = repo.AddDelegation("targets/a/b/c", []data.PublicKey{targetPubKey}, []string{""})
require.NoError(t, err)
// ensure that the changefiles is correct
changes := getChanges(t, repo)
require.Len(t, changes, 2)
require.Equal(t, changelist.ActionCreate, changes[0].Action())
require.Equal(t, "targets/a/b/c", changes[0].Scope())
require.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type())
require.Equal(t, changelist.ActionCreate, changes[1].Action())
require.Equal(t, "targets/a/b/c", changes[1].Scope())
require.Equal(t, changelist.TypeTargetsDelegation, changes[1].Type())
require.Equal(t, "", changes[1].Path())
require.NotEmpty(t, changes[0].Content())
}
// The changefile produced by AddDelegation, when applied, actually adds
// the delegation to the repo (assuming the delegation hierarchy is correct -
// tests for change application validation are in helpers_test.go)
func TestAddDelegationChangefileApplicable(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
require.NotEmpty(t, targetKeyIds)
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
require.NotNil(t, targetPubKey)
// this hierarchy has to be right to be applied
err := repo.AddDelegation("targets/a", []data.PublicKey{targetPubKey}, []string{""})
require.NoError(t, err)
changes := getChanges(t, repo)
require.Len(t, changes, 2)
// ensure that it can be applied correctly
err = applyTargetsChange(repo.tufRepo, changes[0])
require.NoError(t, err)
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
require.Len(t, targetRole.Signed.Delegations.Roles, 1)
require.Len(t, targetRole.Signed.Delegations.Keys, 1)
_, ok := targetRole.Signed.Delegations.Keys[targetPubKey.ID()]
require.True(t, ok)
newDelegationRole := targetRole.Signed.Delegations.Roles[0]
require.Len(t, newDelegationRole.KeyIDs, 1)
require.Equal(t, targetPubKey.ID(), newDelegationRole.KeyIDs[0])
require.Equal(t, "targets/a", newDelegationRole.Name)
}
// TestAddDelegationErrorWritingChanges expects errors writing a change to file
// to be propagated.
func TestAddDelegationErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
targetKeyIds := repo.CryptoService.ListKeys(data.CanonicalTargetsRole)
require.NotEmpty(t, targetKeyIds)
targetPubKey := repo.CryptoService.GetKey(targetKeyIds[0])
require.NotNil(t, targetPubKey)
return repo.AddDelegation("targets/a", []data.PublicKey{targetPubKey}, []string{""})
})
}
// RemoveDelegation rejects attempts to remove invalidly-named delegations,
// but otherwise does not validate the name of the delegation to remove. This
// test ensures that the changefile generated by RemoveDelegation is correct.
func TestRemoveDelegationChangefileValid(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
require.NotNil(t, rootPubKey)
err := repo.RemoveDelegationKeys("root", []string{rootKeyID})
require.Error(t, err)
require.IsType(t, data.ErrInvalidRole{}, err)
require.Empty(t, getChanges(t, repo))
// to demonstrate that so long as the delegation name is valid, the
// existence of the delegation doesn't matter
require.NoError(t, repo.RemoveDelegationKeys("targets/a/b/c", []string{rootKeyID}))
// ensure that the changefile is correct
changes := getChanges(t, repo)
require.Len(t, changes, 1)
require.Equal(t, changelist.ActionUpdate, changes[0].Action())
require.Equal(t, "targets/a/b/c", changes[0].Scope())
require.Equal(t, changelist.TypeTargetsDelegation, changes[0].Type())
require.Equal(t, "", changes[0].Path())
}
// The changefile produced by RemoveDelegationKeys, when applied, actually removes
// the delegation from the repo (assuming the repo exists - tests for
// change application validation are in helpers_test.go)
func TestRemoveDelegationChangefileApplicable(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
require.NotNil(t, rootPubKey)
// add a delegation first so it can be removed
require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{rootPubKey}, []string{""}))
changes := getChanges(t, repo)
require.Len(t, changes, 2)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
require.Len(t, targetRole.Signed.Delegations.Roles, 1)
require.Len(t, targetRole.Signed.Delegations.Keys, 1)
// now remove it
rootKeyCanonicalID, err := utils.CanonicalKeyID(rootPubKey)
require.NoError(t, err)
require.NoError(t, repo.RemoveDelegationKeys("targets/a", []string{rootKeyCanonicalID}))
changes = getChanges(t, repo)
require.Len(t, changes, 3)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[2]))
targetRole = repo.tufRepo.Targets[data.CanonicalTargetsRole]
require.Empty(t, targetRole.Signed.Delegations.Roles)
require.Empty(t, targetRole.Signed.Delegations.Keys)
}
// The changefile with the ClearAllPaths key set, when applied, actually removes
// all paths from the specified delegation in the repo (assuming the repo and delegation exist)
func TestClearAllPathsDelegationChangefileApplicable(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
require.NotNil(t, rootPubKey)
// add a delegation first so it can be removed
require.NoError(t, repo.AddDelegation("targets/a", []data.PublicKey{rootPubKey}, []string{"abc,123,xyz,path"}))
changes := getChanges(t, repo)
require.Len(t, changes, 2)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
// now clear paths it
require.NoError(t, repo.ClearDelegationPaths("targets/a"))
changes = getChanges(t, repo)
require.Len(t, changes, 3)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[2]))
delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles
require.Len(t, delgRoles, 1)
require.Len(t, delgRoles[0].Paths, 0)
}
// TestFullAddDelegationChangefileApplicable generates a single changelist with AddKeys and AddPaths set,
// (in the old style of AddDelegation) and tests that all of its changes are reflected on publish
func TestFullAddDelegationChangefileApplicable(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
require.NotNil(t, rootPubKey)
key2, err := repo.CryptoService.Create("user", repo.gun, data.ECDSAKey)
require.NoError(t, err)
delegationName := "targets/a"
// manually create the changelist object to load multiple keys
tdJSON, err := json.Marshal(&changelist.TufDelegation{
NewThreshold: notary.MinThreshold,
AddKeys: data.KeyList([]data.PublicKey{rootPubKey, key2}),
AddPaths: []string{"abc", "123", "xyz"},
})
change := newCreateDelegationChange(delegationName, tdJSON)
cl, err := changelist.NewFileChangelist(filepath.Join(repo.tufRepoPath, "changelist"))
addChange(cl, change, delegationName)
changes := getChanges(t, repo)
require.Len(t, changes, 1)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles
require.Len(t, delgRoles, 1)
require.Len(t, delgRoles[0].Paths, 3)
require.Len(t, delgRoles[0].KeyIDs, 2)
require.Equal(t, delgRoles[0].Name, delegationName)
}
// TestFullRemoveDelegationChangefileApplicable generates a single changelist with RemoveKeys and RemovePaths set,
// (in the old style of RemoveDelegation) and tests that all of its changes are reflected on publish
func TestFullRemoveDelegationChangefileApplicable(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
rootPubKey := repo.CryptoService.GetKey(rootKeyID)
require.NotNil(t, rootPubKey)
key2, err := repo.CryptoService.Create("user", repo.gun, data.ECDSAKey)
require.NoError(t, err)
key2CanonicalID, err := utils.CanonicalKeyID(key2)
require.NoError(t, err)
delegationName := "targets/a"
require.NoError(t, repo.AddDelegation(delegationName, []data.PublicKey{rootPubKey, key2}, []string{"abc", "123"}))
changes := getChanges(t, repo)
require.Len(t, changes, 2)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[0]))
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[1]))
targetRole := repo.tufRepo.Targets[data.CanonicalTargetsRole]
require.Len(t, targetRole.Signed.Delegations.Roles, 1)
require.Len(t, targetRole.Signed.Delegations.Keys, 2)
// manually create the changelist object to load multiple keys
tdJSON, err := json.Marshal(&changelist.TufDelegation{
RemoveKeys: []string{key2CanonicalID},
RemovePaths: []string{"abc", "123"},
})
change := newUpdateDelegationChange(delegationName, tdJSON)
cl, err := changelist.NewFileChangelist(filepath.Join(repo.tufRepoPath, "changelist"))
addChange(cl, change, delegationName)
changes = getChanges(t, repo)
require.Len(t, changes, 3)
require.NoError(t, applyTargetsChange(repo.tufRepo, changes[2]))
delgRoles := repo.tufRepo.Targets[data.CanonicalTargetsRole].Signed.Delegations.Roles
require.Len(t, delgRoles, 1)
require.Len(t, delgRoles[0].Paths, 0)
require.Len(t, delgRoles[0].KeyIDs, 1)
}
// TestRemoveDelegationErrorWritingChanges expects errors writing a change to
// file to be propagated.
func TestRemoveDelegationErrorWritingChanges(t *testing.T) {
testErrorWritingChangefiles(t, func(repo *NotaryRepository) error {
return repo.RemoveDelegationKeysAndPaths("targets/a", []string{""}, []string{})
})
}
// TestBootstrapClientBadURL checks that bootstrapClient correctly
// returns an error when the URL is valid but does not point to
// a TUF server
func TestBootstrapClientBadURL(t *testing.T) {
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
repo, err := NewNotaryRepository(
tempBaseDir,
"testGun",
"http://localhost:9998",
http.DefaultTransport,
passphraseRetriever,
trustpinning.TrustPinConfig{},
)
require.NoError(t, err, "error creating repo: %s", err)
c, err := repo.bootstrapClient(false)
require.Nil(t, c)
require.Error(t, err)
c, err2 := repo.bootstrapClient(true)
require.Nil(t, c)
require.Error(t, err2)
// same error should be returned because we don't have local data
// and are requesting remote root regardless of checkInitialized
// value
require.EqualError(t, err, err2.Error())
}
// TestBootstrapClientInvalidURL checks that bootstrapClient correctly
// returns an error when the URL is valid but does not point to
// a TUF server
func TestBootstrapClientInvalidURL(t *testing.T) {
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
require.NoError(t, err, "failed to create a temporary directory: %s", err)
repo, err := NewNotaryRepository(
tempBaseDir,
"testGun",
"#!*)&!)#*^%!#)%^!#",
http.DefaultTransport,
passphraseRetriever,
trustpinning.TrustPinConfig{},
)
require.NoError(t, err, "error creating repo: %s", err)
c, err := repo.bootstrapClient(false)
require.Nil(t, c)
require.Error(t, err)
c, err2 := repo.bootstrapClient(true)
require.Nil(t, c)
require.Error(t, err2)
// same error should be returned because we don't have local data
// and are requesting remote root regardless of checkInitialized
// value
require.EqualError(t, err, err2.Error())
}
func TestPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t *testing.T) {
testPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t, false)
testPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t, true)
}
func testPublishTargetsDelegationCanUseUserKeyWithArbitraryRole(t *testing.T, x509 bool) {
gun := "docker.com/notary"
ts := fullTestServer(t)
defer ts.Close()
// this is the original repo - it owns the root/targets keys and creates
// the delegation to which it doesn't have the key (so server snapshot
// signing would be required)
ownerRepo, _ := initializeRepo(t, data.ECDSAKey, gun, ts.URL, true)
defer os.RemoveAll(ownerRepo.baseDir)
// this is a user, or otherwise a repo that only has access to the delegation
// key so it can publish targets to the delegated role
delgRepo, _ := newRepoToTestRepo(t, ownerRepo, true)
defer os.RemoveAll(delgRepo.baseDir)
// create a key on the owner repo
aKey := createKey(t, ownerRepo, "user", x509)
_, err := utils.CanonicalKeyID(aKey)
require.NoError(t, err)
// create a key on the delegated repo
bKey := createKey(t, delgRepo, "notARealRoleName", x509)
_, err = utils.CanonicalKeyID(bKey)
require.NoError(t, err)
// clear metadata and unencrypted private key cache
var ownerRec, delgRec *passRoleRecorder
ownerRepo, ownerRec = newRepoToTestRepo(t, ownerRepo, false)
delgRepo, delgRec = newRepoToTestRepo(t, delgRepo, false)
// owner creates delegations, adds the delegated key to them, and publishes them
require.NoError(t,
ownerRepo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
"error creating delegation")
require.NoError(t,
ownerRepo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}),
"error creating delegation")
require.NoError(t, ownerRepo.Publish())
// delegation parents all get signed
ownerRec.requireAsked(t, []string{data.CanonicalTargetsRole, "targets/a"})
// delegated repo now publishes to delegated roles, but it will need
// to download those roles first, since it doesn't know about them
requirePublishToRolesSucceeds(t, delgRepo, []string{"targets/a/b"},
[]string{"targets/a/b"})
delgRec.requireAsked(t, []string{"targets/a/b"})
}
// TestDeleteRepo tests that local repo data, certificate, and keys are deleted from the client library call
func TestDeleteRepo(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// Assert initialization was successful before we delete
requireRepoHasExpectedKeys(t, repo, rootKeyID, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true)
// Delete all client trust data for repo
err := repo.DeleteTrustData()
require.NoError(t, err)
// Assert no metadata for this repo exists locally
requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, false)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, false)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, false)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTimestampRole, false)
// Assert keys for this repo exist locally
requireRepoHasExpectedKeys(t, repo, rootKeyID, true)
}
type brokenRemoveFilestore struct {
store.MetadataStore
}
func (s *brokenRemoveFilestore) RemoveAll() error {
return fmt.Errorf("can't remove from this broken filestore")
}
// TestDeleteRepoBadFilestore tests that we properly error when trying to remove against a faulty filestore
func TestDeleteRepoBadFilestore(t *testing.T) {
gun := "docker.com/notary"
ts, _, _ := simpleTestServer(t)
defer ts.Close()
repo, rootKeyID := initializeRepo(t, data.ECDSAKey, gun, ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// Assert initialization was successful before we delete
requireRepoHasExpectedKeys(t, repo, rootKeyID, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalRootRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalTargetsRole, true)
requireRepoHasExpectedMetadata(t, repo, data.CanonicalSnapshotRole, true)
// Make the filestore faulty on remove
repo.fileStore = &brokenRemoveFilestore{repo.fileStore}
// Delete all client trust data for repo, require an error on the filestore removal
err := repo.DeleteTrustData()
require.Error(t, err)
}
// Test that we get a correct list of roles with keys and signatures
func TestListRoles(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)
require.NoError(t, repo.Publish())
rolesWithSigs, err := repo.ListRoles()
require.NoError(t, err)
// Should only have base roles at this point
require.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 {
require.Len(t, role.Signatures, 1)
require.Len(t, role.KeyIDs, 1)
require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0])
}
// Create a delegation on the top level
aKey := createKey(t, repo, "user", true)
require.NoError(t,
repo.AddDelegation("targets/a", []data.PublicKey{aKey}, []string{""}),
"error creating delegation")
require.NoError(t, repo.Publish())
rolesWithSigs, err = repo.ListRoles()
require.NoError(t, err)
require.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" {
require.Nil(t, role.Signatures)
} else {
require.Len(t, role.Signatures, 1)
require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0])
}
require.Len(t, role.KeyIDs, 1)
}
addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt", "targets/a")
require.NoError(t, repo.Publish())
rolesWithSigs, err = repo.ListRoles()
require.NoError(t, err)
require.Len(t, rolesWithSigs, len(data.BaseRoles)+1)
// The delegation should have a signature now
for _, role := range rolesWithSigs {
require.Len(t, role.Signatures, 1)
require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0])
require.Len(t, role.KeyIDs, 1)
}
// Create another delegation, one level further
bKey := createKey(t, repo, "user", true)
require.NoError(t,
repo.AddDelegation("targets/a/b", []data.PublicKey{bKey}, []string{""}),
"error creating delegation")
require.NoError(t, repo.Publish())
rolesWithSigs, err = repo.ListRoles()
require.NoError(t, err)
require.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" {
require.Nil(t, role.Signatures)
} else {
require.Len(t, role.Signatures, 1)
require.Equal(t, role.Signatures[0].KeyID, role.KeyIDs[0])
}
require.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)
require.NoError(t, repo2.Publish())
// repo2 only has the base roles
rolesWithSigs2, err := repo2.ListRoles()
require.NoError(t, err)
require.Len(t, rolesWithSigs2, len(data.BaseRoles))
// original repo stays in same state (base roles + 2 delegations)
rolesWithSigs, err = repo.ListRoles()
require.NoError(t, err)
require.Len(t, rolesWithSigs, len(data.BaseRoles)+2)
}
func TestGetAllTargetInfo(t *testing.T) {
ts, mux, keys := simpleTestServer(t)
defer ts.Close()
rootType := data.ECDSAKey
repo, _ := initializeRepo(t, rootType, "docker.com/notary", ts.URL, false)
defer os.RemoveAll(repo.baseDir)
// tests need to manually bootstrap timestamp as client doesn't generate it
err := repo.tufRepo.InitTimestamp()
require.NoError(t, err, "error creating repository: %s", err)
// add latest and current to targets role
targetsLatestTarget := addTarget(t, repo, "latest", "../fixtures/intermediate-ca.crt")
targetsCurrentTarget := addTarget(t, repo, "current", "../fixtures/intermediate-ca.crt")
// setup delegated targets/level1 role with targets current and other
k, err := repo.CryptoService.Create("targets/level1", repo.gun, rootType)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level1", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1", []string{""}, []string{}, false)
require.NoError(t, err)
level1CurrentTarget := addTarget(t, repo, "current", "../fixtures/root-ca.crt", "targets/level1")
level1OtherTarget := addTarget(t, repo, "other", "../fixtures/root-ca.crt", "targets/level1")
// setup delegated targets/level2 role with targets current and level2
k, err = repo.CryptoService.Create("targets/level2", repo.gun, rootType)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level2", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level2", []string{""}, []string{}, false)
require.NoError(t, err)
level2CurrentTarget := addTarget(t, repo, "current", "../fixtures/notary-server.crt", "targets/level2")
level2Level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level2")
// Apply the changelist. Normally, this would be done by Publish
// load the changelist for this repo
cl, err := changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo, then clear it
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
require.NoError(t, cl.Clear(""))
_, ok := repo.tufRepo.Targets["targets/level1"].Signed.Targets["current"]
require.True(t, ok)
_, ok = repo.tufRepo.Targets["targets/level1"].Signed.Targets["other"]
require.True(t, ok)
_, ok = repo.tufRepo.Targets["targets/level2"].Signed.Targets["level2"]
require.True(t, ok)
// setup delegated targets/level1/level2 role separately, which can only modify paths prefixed with "level2"
// add level2 to targets/level1/level2
k, err = repo.CryptoService.Create("targets/level1/level2", repo.gun, rootType)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationKeys("targets/level1/level2", []data.PublicKey{k}, []string{}, 1)
require.NoError(t, err)
err = repo.tufRepo.UpdateDelegationPaths("targets/level1/level2", []string{"level2"}, []string{}, false)
require.NoError(t, err)
level1Level2Level2Target := addTarget(t, repo, "level2", "../fixtures/notary-server.crt", "targets/level1/level2")
// load the changelist for this repo
cl, err = changelist.NewFileChangelist(
filepath.Join(repo.baseDir, "tuf", filepath.FromSlash(repo.gun), "changelist"))
require.NoError(t, err, "could not open changelist")
// apply the changelist to the repo
err = applyChangelist(repo.tufRepo, cl)
require.NoError(t, err, "could not apply changelist")
// check the changelist was applied
_, ok = repo.tufRepo.Targets["targets/level1/level2"].Signed.Targets["level2"]
require.True(t, ok)
fakeServerData(t, repo, mux, keys)
// At this point, we have the following view of targets:
// current - signed by targets, targets/level1, and targets/level2, all with different hashes
// other - signed by targets/level1
// latest - signed by targets
// level2 - signed by targets/level2 and targets/level1/level2, with the same hash
// Positive cases
targetData, err := repo.GetAllTargetMetadataByName("current")
require.NoError(t, err)
require.NotNil(t, targetData)
require.Len(t, targetData, 3)
require.Contains(t, targetData, &TargetWithRole{Target: *targetsCurrentTarget, Role: data.CanonicalTargetsRole})
require.Contains(t, targetData, &TargetWithRole{Target: *level1CurrentTarget, Role: "targets/level1"})
require.Contains(t, targetData, &TargetWithRole{Target: *level2CurrentTarget, Role: "targets/level2"})
targetData, err = repo.GetAllTargetMetadataByName("other")
require.NoError(t, err)
require.NotNil(t, targetData)
require.Len(t, targetData, 1)
require.Contains(t, targetData, &TargetWithRole{Target: *level1OtherTarget, Role: "targets/level1"})
targetData, err = repo.GetAllTargetMetadataByName("latest")
require.NoError(t, err)
require.NotNil(t, targetData)
require.Len(t, targetData, 1)
require.Contains(t, targetData, &TargetWithRole{Target: *targetsLatestTarget, Role: data.CanonicalTargetsRole})
targetData, err = repo.GetAllTargetMetadataByName("level2")
require.NoError(t, err)
require.NotNil(t, targetData)
require.Len(t, targetData, 2)
require.Contains(t, targetData, &TargetWithRole{Target: *level2Level2Target, Role: "targets/level2"})
require.Contains(t, targetData, &TargetWithRole{Target: *level1Level2Level2Target, Role: "targets/level1/level2"})
// nonexistent targets
targetData, err = repo.GetAllTargetMetadataByName("level23")
require.Error(t, err)
require.Nil(t, targetData)
targetData, err = repo.GetAllTargetMetadataByName("invalid")
require.Error(t, err)
require.Nil(t, targetData)
}