package image import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/docker/cli/e2e/internal/fixtures" "github.com/docker/cli/internal/test/environment" "github.com/docker/cli/internal/test/output" "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/golden" "gotest.tools/v3/icmd" "gotest.tools/v3/skip" ) const ( notary = "/usr/local/bin/notary" pubkey1 = "./testdata/notary/delgkey1.crt" privkey1 = "./testdata/notary/delgkey1.key" pubkey2 = "./testdata/notary/delgkey2.crt" privkey2 = "./testdata/notary/delgkey2.key" pubkey3 = "./testdata/notary/delgkey3.crt" privkey3 = "./testdata/notary/delgkey3.key" pubkey4 = "./testdata/notary/delgkey4.crt" privkey4 = "./testdata/notary/delgkey4.key" ) func TestPushWithContentTrust(t *testing.T) { skip.If(t, environment.RemoteDaemon()) dir := fixtures.SetupConfigFile(t) defer dir.Remove() image := createImage(t, registryPrefix, "trust-push", "latest") result := icmd.RunCmd(icmd.Command("docker", "push", image), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, fixtures.WithPassphrase("foo", "bar"), ) result.Assert(t, icmd.Success) golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden") output.Assert(t, result.Stdout(), map[int]func(string) error{ 0: output.Equals("The push refers to repository [registry:5000/trust-push]"), 1: output.Equals("5bef08742407: Preparing"), 3: output.Equals("latest: digest: sha256:641b95ddb2ea9dc2af1a0113b6b348ebc20872ba615204fbe12148e98fd6f23d size: 528"), 4: output.Equals("Signing and pushing trust metadata"), 5: output.Equals(`Finished initializing "registry:5000/trust-push"`), 6: output.Equals("Successfully signed registry:5000/trust-push:latest"), }) } func TestPushWithContentTrustUnreachableServer(t *testing.T) { skip.If(t, environment.RemoteDaemon()) dir := fixtures.SetupConfigFile(t) defer dir.Remove() image := createImage(t, registryPrefix, "trust-push-unreachable", "latest") result := icmd.RunCmd(icmd.Command("docker", "push", image), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotaryServer("https://invalidnotaryserver"), ) result.Assert(t, icmd.Expected{ ExitCode: 1, Err: "error contacting notary server", }) } func TestPushWithContentTrustExistingTag(t *testing.T) { skip.If(t, environment.RemoteDaemon()) dir := fixtures.SetupConfigFile(t) defer dir.Remove() image := createImage(t, registryPrefix, "trust-push-existing", "latest") result := icmd.RunCmd(icmd.Command("docker", "push", image)) result.Assert(t, icmd.Success) result = icmd.RunCmd(icmd.Command("docker", "push", image), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, fixtures.WithPassphrase("foo", "bar"), ) result.Assert(t, icmd.Expected{ Out: "Signing and pushing trust metadata", }) // Re-push result = icmd.RunCmd(icmd.Command("docker", "push", image), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, fixtures.WithPassphrase("foo", "bar"), ) result.Assert(t, icmd.Expected{ Out: "Signing and pushing trust metadata", }) } func TestPushWithContentTrustReleasesDelegationOnly(t *testing.T) { skip.If(t, environment.RemoteDaemon()) role := "targets/releases" dir := fixtures.SetupConfigFile(t) defer dir.Remove() copyPrivateKey(t, dir.Join("trust", "private"), privkey1) notaryDir := setupNotaryConfig(t, dir) defer notaryDir.Remove() homeDir := fs.NewDir(t, "push_test_home") defer notaryDir.Remove() baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-delegation") targetRef := fmt.Sprintf("%s:%s", baseRef, "latest") // Init repository notaryInit(t, notaryDir, homeDir, baseRef) // Add delegation key (public key) notaryAddDelegation(t, notaryDir, homeDir, baseRef, role, pubkey1) // Publish it notaryPublish(t, notaryDir, homeDir, baseRef) // Import private key notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, role, privkey1) // Tag & push with content trust icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success) result := icmd.RunCmd(icmd.Command("docker", "push", targetRef), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, fixtures.WithPassphrase("foo", "foo"), ) result.Assert(t, icmd.Expected{ Out: "Signing and pushing trust metadata", }) targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, role) assert.Assert(t, targetsInRole["latest"] == role, "%v", targetsInRole) targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets") assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole) result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, ) result.Assert(t, icmd.Success) } func TestPushWithContentTrustSignsAllFirstLevelRolesWeHaveKeysFor(t *testing.T) { skip.If(t, environment.RemoteDaemon()) dir := fixtures.SetupConfigFile(t) defer dir.Remove() copyPrivateKey(t, dir.Join("trust", "private"), privkey1) copyPrivateKey(t, dir.Join("trust", "private"), privkey2) copyPrivateKey(t, dir.Join("trust", "private"), privkey3) notaryDir := setupNotaryConfig(t, dir) defer notaryDir.Remove() homeDir := fs.NewDir(t, "push_test_home") defer notaryDir.Remove() baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-first-roles") targetRef := fmt.Sprintf("%s:%s", baseRef, "latest") // Init repository notaryInit(t, notaryDir, homeDir, baseRef) // Add delegation key (public key) notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1", pubkey1) notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role2", pubkey2) notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role3", pubkey3) notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1/subrole", pubkey3) // Import private key notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1", privkey1) notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role2", privkey2) notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1/subrole", privkey3) // Publish it notaryPublish(t, notaryDir, homeDir, baseRef) // Tag & push with content trust icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success) result := icmd.RunCmd(icmd.Command("docker", "push", targetRef), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, fixtures.WithPassphrase("foo", "foo"), ) result.Assert(t, icmd.Expected{ Out: "Signing and pushing trust metadata", }) // check to make sure that the target has been added to targets/role1 and targets/role2, and // not targets (because there are delegations) or targets/role3 (due to missing key) or // targets/role1/subrole (due to it being a second level delegation) targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role1") assert.Assert(t, targetsInRole["latest"] == "targets/role1", "%v", targetsInRole) targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role2") assert.Assert(t, targetsInRole["latest"] == "targets/role2", "%v", targetsInRole) targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets") assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole) assert.NilError(t, os.RemoveAll(filepath.Join(dir.Join("trust")))) // Try to pull, should fail because non of these are the release role // FIXME(vdemeester) should be unit test result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, ) result.Assert(t, icmd.Expected{ ExitCode: 1, }) } func TestPushWithContentTrustSignsForRolesWithKeysAndValidPaths(t *testing.T) { skip.If(t, environment.RemoteDaemon()) dir := fixtures.SetupConfigFile(t) defer dir.Remove() copyPrivateKey(t, dir.Join("trust", "private"), privkey1) copyPrivateKey(t, dir.Join("trust", "private"), privkey2) copyPrivateKey(t, dir.Join("trust", "private"), privkey3) copyPrivateKey(t, dir.Join("trust", "private"), privkey4) notaryDir := setupNotaryConfig(t, dir) defer notaryDir.Remove() homeDir := fs.NewDir(t, "push_test_home") defer notaryDir.Remove() baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-keys-valid-paths") targetRef := fmt.Sprintf("%s:%s", baseRef, "latest") // Init repository notaryInit(t, notaryDir, homeDir, baseRef) // Add delegation key (public key) notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1", pubkey1, "l", "z") notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role2", pubkey2, "x", "y") notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role3", pubkey3, "latest") notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role4", pubkey4, "latest") // Import private keys (except 3rd key) notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1", privkey1) notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role2", privkey2) notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role4", privkey4) // Publish it notaryPublish(t, notaryDir, homeDir, baseRef) // Tag & push with content trust icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success) result := icmd.RunCmd(icmd.Command("docker", "push", targetRef), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, fixtures.WithPassphrase("foo", "foo"), ) result.Assert(t, icmd.Expected{ Out: "Signing and pushing trust metadata", }) // check to make sure that the target has been added to targets/role1 and targets/role4, and // not targets (because there are delegations) or targets/role2 (due to path restrictions) or // targets/role3 (due to missing key) targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role1") assert.Assert(t, targetsInRole["latest"] == "targets/role1", "%v", targetsInRole) targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role4") assert.Assert(t, targetsInRole["latest"] == "targets/role4", "%v", targetsInRole) targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets") assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole) assert.NilError(t, os.RemoveAll(filepath.Join(dir.Join("trust")))) // Try to pull, should fail because non of these are the release role // FIXME(vdemeester) should be unit test result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef), fixtures.WithConfig(dir.Path()), fixtures.WithTrust, fixtures.WithNotary, ) result.Assert(t, icmd.Expected{ ExitCode: 1, }) } func createImage(t *testing.T, registryPrefix, repo, tag string) string { image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag) icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success) return image } //nolint: unparam func withNotaryPassphrase(pwd string) func(*icmd.Cmd) { return func(c *icmd.Cmd) { c.Env = append(c.Env, []string{ fmt.Sprintf("NOTARY_ROOT_PASSPHRASE=%s", pwd), fmt.Sprintf("NOTARY_TARGETS_PASSPHRASE=%s", pwd), fmt.Sprintf("NOTARY_SNAPSHOT_PASSPHRASE=%s", pwd), fmt.Sprintf("NOTARY_DELEGATION_PASSPHRASE=%s", pwd), }...) } } func notaryImportPrivateKey(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role, privkey string) { icmd.RunCmd( icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "key", "import", privkey, "-g", baseRef, "-r", role), withNotaryPassphrase("foo"), fixtures.WithHome(homeDir.Path()), ).Assert(t, icmd.Success) } func notaryPublish(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef string) { icmd.RunCmd( icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "publish", baseRef), withNotaryPassphrase("foo"), fixtures.WithHome(homeDir.Path()), ).Assert(t, icmd.Success) } func notaryAddDelegation(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role, pubkey string, paths ...string) { pathsArg := "--all-paths" if len(paths) > 0 { pathsArg = "--paths=" + strings.Join(paths, ",") } icmd.RunCmd( icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "delegation", "add", baseRef, role, pubkey, pathsArg), withNotaryPassphrase("foo"), fixtures.WithHome(homeDir.Path()), ).Assert(t, icmd.Success) } func notaryInit(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef string) { icmd.RunCmd( icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "init", baseRef), withNotaryPassphrase("foo"), fixtures.WithHome(homeDir.Path()), ).Assert(t, icmd.Success) } func notaryListTargetsInRole(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role string) map[string]string { result := icmd.RunCmd( icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "list", baseRef, "-r", role), fixtures.WithHome(homeDir.Path()), ) out := result.Combined() // should look something like: // NAME DIGEST SIZE (BYTES) ROLE // ------------------------------------------------------------------------------------------------------ // latest 24a36bbc059b1345b7e8be0df20f1b23caa3602e85d42fff7ecd9d0bd255de56 1377 targets targets := make(map[string]string) // no target lines := strings.Split(strings.TrimSpace(out), "\n") if len(lines) == 1 && strings.Contains(out, "No targets present in this repository.") { return targets } // otherwise, there is at least one target assert.Assert(t, len(lines) >= 3, "output is %s", out) for _, line := range lines[2:] { tokens := strings.Fields(line) assert.Assert(t, len(tokens) == 4) targets[tokens[0]] = tokens[3] } return targets } func setupNotaryConfig(t *testing.T, dockerConfigDir fs.Dir) *fs.Dir { return fs.NewDir(t, "notary_test", fs.WithMode(0700), fs.WithFile("client-config.json", fmt.Sprintf(` { "trust_dir": "%s", "remote_server": { "url": "%s" } }`, dockerConfigDir.Join("trust"), fixtures.NotaryURL)), ) } func copyPrivateKey(t *testing.T, dir, source string) { icmd.RunCommand("/bin/cp", source, dir).Assert(t, icmd.Success) }