diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index 392b2243f4..eee87cb39b 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -24,6 +24,7 @@ import ( "github.com/docker/notary/server/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/utils" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -50,6 +51,11 @@ func runCommand(t *testing.T, tempDir string, args ...string) (string, error) { output, err := ioutil.ReadAll(b) assert.NoError(t, err) + // Clean up state to mimic running a fresh command next time + for _, command := range cmd.Commands() { + command.ResetFlags() + } + return string(output), retErr } @@ -362,6 +368,164 @@ func TestClientDelegationsInteraction(t *testing.T) { assert.Contains(t, output, "No delegations present in this repository.") } +// Initialize repo and test publishing targets with delegation roles +func TestClientDelegationsPublishing(t *testing.T) { + setUp(t) + + tempDir := tempDirWithConfig(t, "{}") + defer os.RemoveAll(tempDir) + + server := setupServer() + defer server.Close() + + // Setup certificate for delegation role + tempFile, err := ioutil.TempFile("/tmp", "pemfile") + assert.NoError(t, err) + + privKey, err := trustmanager.GenerateRSAKey(rand.Reader, 2048) + assert.NoError(t, err) + privKeyBytesNoRole, err := trustmanager.KeyToPEM(privKey, "") + assert.NoError(t, err) + privKeyBytesWithRole, err := trustmanager.KeyToPEM(privKey, "user") + assert.NoError(t, err) + startTime := time.Now() + endTime := startTime.AddDate(10, 0, 0) + cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime) + assert.NoError(t, err) + + _, err = tempFile.Write(trustmanager.CertToPEM(cert)) + assert.NoError(t, err) + tempFile.Close() + defer os.Remove(tempFile.Name()) + + rawPubBytes, _ := ioutil.ReadFile(tempFile.Name()) + parsedPubKey, _ := trustmanager.ParsePEMPublicKey(rawPubBytes) + canonicalKeyID, err := utils.CanonicalKeyID(parsedPubKey) + assert.NoError(t, err) + + // Set up targets for publishing + tempTargetFile, err := ioutil.TempFile("/tmp", "targetfile") + assert.NoError(t, err) + tempTargetFile.Close() + defer os.Remove(tempTargetFile.Name()) + + var target = "sdgkadga" + + var output string + + // init repo + _, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun") + assert.NoError(t, err) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // list delegations - none yet + output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") + assert.NoError(t, err) + assert.Contains(t, output, "No delegations present in this repository.") + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // validate that we have all keys, including snapshot + assertNumKeys(t, tempDir, 1, 2, true) + + // rotate the snapshot key to server + output, err = runCommand(t, tempDir, "-s", server.URL, "key", "rotate", "gun", "-r", "--key-type", "snapshot") + assert.NoError(t, err) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // validate that we lost the snapshot signing key + _, signingKeyIDs := assertNumKeys(t, tempDir, 1, 1, true) + targetKeyID := signingKeyIDs[0] + + // add new valid delegation with single new cert + output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/releases", tempFile.Name(), "--paths", "\"\"") + assert.NoError(t, err) + assert.Contains(t, output, "Addition of delegation role") + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // list delegations - we should see our one delegation + output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun") + assert.NoError(t, err) + assert.NotContains(t, output, "No delegations present in this repository.") + + // remove the targets key to demonstrate that delegates don't need this key + keyDir := filepath.Join(tempDir, "private", "tuf_keys") + assert.NoError(t, os.Remove(filepath.Join(keyDir, "gun", targetKeyID+".key"))) + + // Note that we need to use the canonical key ID, followed by the base of the role here + err = ioutil.WriteFile(filepath.Join(keyDir, canonicalKeyID+"_releases.key"), privKeyBytesNoRole, 0700) + assert.NoError(t, err) + + // add a target using the delegation -- will only add to targets/releases + _, err = runCommand(t, tempDir, "add", "gun", target, tempTargetFile.Name(), "--roles", "targets/releases") + assert.NoError(t, err) + + // list targets for targets/releases - we should see no targets until we publish + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") + assert.NoError(t, err) + assert.Contains(t, output, "No targets") + + output, err = runCommand(t, tempDir, "-s", server.URL, "status", "gun") + assert.NoError(t, err) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // list targets for targets/releases - we should see our target! + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") + assert.NoError(t, err) + assert.Contains(t, output, "targets/releases") + + // remove the target for this role only + _, err = runCommand(t, tempDir, "remove", "gun", target, "--roles", "targets/releases") + assert.NoError(t, err) + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // list targets for targets/releases - we should see no targets + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") + assert.NoError(t, err) + assert.Contains(t, output, "No targets present") + + // Try adding a target with a different key style - private/tuf_keys/canonicalKeyID.key with "user" set as the "role" PEM header + // First remove the old key and add the new style + assert.NoError(t, os.Remove(filepath.Join(keyDir, canonicalKeyID+"_releases.key"))) + err = ioutil.WriteFile(filepath.Join(keyDir, canonicalKeyID+".key"), privKeyBytesWithRole, 0700) + assert.NoError(t, err) + + // add a target using the delegation -- will only add to targets/releases + _, err = runCommand(t, tempDir, "add", "gun", target, tempTargetFile.Name(), "--roles", "targets/releases") + assert.NoError(t, err) + + // list targets for targets/releases - we should see no targets until we publish + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") + assert.NoError(t, err) + assert.Contains(t, output, "No targets") + + // publish repo + _, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun") + assert.NoError(t, err) + + // list targets for targets/releases - we should see our target! + output, err = runCommand(t, tempDir, "-s", server.URL, "list", "gun", "--roles", "targets/releases") + assert.NoError(t, err) + assert.Contains(t, output, "targets/releases") +} + // Splits a string into lines, and returns any lines that are not empty ( // striped of whitespace) func splitLines(chunk string) []string { diff --git a/cmd/notary/main.go b/cmd/notary/main.go index 11baffd33b..4ece679df0 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -22,6 +22,7 @@ const ( var ( debug bool verbose bool + roles []string trustDir string configFile string remoteTrustServer string @@ -127,8 +128,11 @@ func setupCommand(notaryCmd *cobra.Command) { notaryCmd.AddCommand(cmdDelegationGenerator.GetCommand()) notaryCmd.AddCommand(cmdCert) notaryCmd.AddCommand(cmdTufInit) + cmdTufList.Flags().StringSliceVarP(&roles, "roles", "r", nil, "Delegation roles to list targets for (will shadow targets role)") notaryCmd.AddCommand(cmdTufList) + cmdTufAdd.Flags().StringSliceVarP(&roles, "roles", "r", nil, "Delegation roles to add this target to") notaryCmd.AddCommand(cmdTufAdd) + cmdTufRemove.Flags().StringSliceVarP(&roles, "roles", "r", nil, "Delegation roles to remove this target from") notaryCmd.AddCommand(cmdTufRemove) notaryCmd.AddCommand(cmdTufStatus) notaryCmd.AddCommand(cmdTufPublish) diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 6b05c82c77..fbc48236b5 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -104,7 +104,8 @@ func tufAdd(cmd *cobra.Command, args []string) { if err != nil { fatalf(err.Error()) } - err = nRepo.AddTarget(target) + // If roles is empty, we default to adding to targets + err = nRepo.AddTarget(target, roles...) if err != nil { fatalf(err.Error()) } @@ -163,8 +164,9 @@ func tufList(cmd *cobra.Command, args []string) { fatalf(err.Error()) } - // Retreive the remote list of signed targets - targetList, err := nRepo.ListTargets(data.CanonicalTargetsRole, "targets/releases") + // Retrieve the remote list of signed targets, prioritizing the passed-in list over targets + roles = append(roles, data.CanonicalTargetsRole) + targetList, err := nRepo.ListTargets(roles...) if err != nil { fatalf(err.Error()) } @@ -265,7 +267,8 @@ func tufRemove(cmd *cobra.Command, args []string) { if err != nil { fatalf(err.Error()) } - err = repo.RemoveTarget(targetName) + // If roles is empty, we default to removing from targets + err = repo.RemoveTarget(targetName, roles...) if err != nil { fatalf(err.Error()) } diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index f39ca8eb22..d44dc5d0de 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -470,12 +470,17 @@ func KeyToPEM(privKey data.PrivateKey, role string) ([]byte, error) { return nil, err } - block := &pem.Block{ - Type: bt, - Headers: map[string]string{ + headers := map[string]string{} + if role != "" { + headers = map[string]string{ "role": role, - }, - Bytes: privKey.Private(), + } + } + + block := &pem.Block{ + Type: bt, + Headers: headers, + Bytes: privKey.Private(), } return pem.EncodeToMemory(block), nil