github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/trust/sign_test.go (about)

     1  package trust
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io/ioutil"
     7  	"os"
     8  	"runtime"
     9  	"testing"
    10  
    11  	"github.com/docker/cli/cli/config"
    12  	"github.com/docker/cli/cli/trust"
    13  	"github.com/docker/cli/internal/test"
    14  	notaryfake "github.com/docker/cli/internal/test/notary"
    15  	"github.com/theupdateframework/notary"
    16  	"github.com/theupdateframework/notary/client"
    17  	"github.com/theupdateframework/notary/client/changelist"
    18  	"github.com/theupdateframework/notary/passphrase"
    19  	"github.com/theupdateframework/notary/trustpinning"
    20  	"github.com/theupdateframework/notary/tuf/data"
    21  	"gotest.tools/v3/assert"
    22  	is "gotest.tools/v3/assert/cmp"
    23  	"gotest.tools/v3/skip"
    24  )
    25  
    26  const passwd = "password"
    27  
    28  func TestTrustSignCommandErrors(t *testing.T) {
    29  	testCases := []struct {
    30  		name          string
    31  		args          []string
    32  		expectedError string
    33  	}{
    34  		{
    35  			name:          "not-enough-args",
    36  			expectedError: "requires exactly 1 argument",
    37  		},
    38  		{
    39  			name:          "too-many-args",
    40  			args:          []string{"image", "tag"},
    41  			expectedError: "requires exactly 1 argument",
    42  		},
    43  		{
    44  			name:          "sha-reference",
    45  			args:          []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
    46  			expectedError: "invalid repository name",
    47  		},
    48  		{
    49  			name:          "invalid-img-reference",
    50  			args:          []string{"ALPINE:latest"},
    51  			expectedError: "invalid reference format",
    52  		},
    53  		{
    54  			name:          "no-tag",
    55  			args:          []string{"reg/img"},
    56  			expectedError: "No tag specified for reg/img",
    57  		},
    58  		{
    59  			name:          "digest-reference",
    60  			args:          []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"},
    61  			expectedError: "cannot use a digest reference for IMAGE:TAG",
    62  		},
    63  	}
    64  	// change to a tmpdir
    65  	tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
    66  	assert.NilError(t, err)
    67  	defer os.RemoveAll(tmpDir)
    68  	config.SetDir(tmpDir)
    69  	for _, tc := range testCases {
    70  		cmd := newSignCommand(
    71  			test.NewFakeCli(&fakeClient{}))
    72  		cmd.SetArgs(tc.args)
    73  		cmd.SetOut(ioutil.Discard)
    74  		assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
    75  	}
    76  }
    77  
    78  func TestTrustSignCommandOfflineErrors(t *testing.T) {
    79  	cli := test.NewFakeCli(&fakeClient{})
    80  	cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository)
    81  	cmd := newSignCommand(cli)
    82  	cmd.SetArgs([]string{"reg-name.io/image:tag"})
    83  	cmd.SetOut(ioutil.Discard)
    84  	assert.ErrorContains(t, cmd.Execute(), "client is offline")
    85  }
    86  
    87  func TestGetOrGenerateNotaryKey(t *testing.T) {
    88  	tmpDir, err := ioutil.TempDir("", "notary-test-")
    89  	assert.NilError(t, err)
    90  	defer os.RemoveAll(tmpDir)
    91  
    92  	notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
    93  	assert.NilError(t, err)
    94  
    95  	// repo is empty, try making a root key
    96  	rootKeyA, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
    97  	assert.NilError(t, err)
    98  	assert.Check(t, rootKeyA != nil)
    99  
   100  	// we should only have one newly generated key
   101  	allKeys := notaryRepo.GetCryptoService().ListAllKeys()
   102  	assert.Check(t, is.Len(allKeys, 1))
   103  	assert.Check(t, notaryRepo.GetCryptoService().GetKey(rootKeyA.ID()) != nil)
   104  
   105  	// this time we should get back the same key if we ask for another root key
   106  	rootKeyB, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
   107  	assert.NilError(t, err)
   108  	assert.Check(t, rootKeyB != nil)
   109  
   110  	// we should only have one newly generated key
   111  	allKeys = notaryRepo.GetCryptoService().ListAllKeys()
   112  	assert.Check(t, is.Len(allKeys, 1))
   113  	assert.Check(t, notaryRepo.GetCryptoService().GetKey(rootKeyB.ID()) != nil)
   114  
   115  	// The key we retrieved should be identical to the one we generated
   116  	assert.Check(t, is.DeepEqual(rootKeyA.Public(), rootKeyB.Public()))
   117  
   118  	// Now also try with a delegation key
   119  	releasesKey, err := getOrGenerateNotaryKey(notaryRepo, trust.ReleasesRole)
   120  	assert.NilError(t, err)
   121  	assert.Check(t, releasesKey != nil)
   122  
   123  	// we should now have two keys
   124  	allKeys = notaryRepo.GetCryptoService().ListAllKeys()
   125  	assert.Check(t, is.Len(allKeys, 2))
   126  	assert.Check(t, notaryRepo.GetCryptoService().GetKey(releasesKey.ID()) != nil)
   127  	// The key we retrieved should be identical to the one we generated
   128  	assert.Check(t, releasesKey != rootKeyA)
   129  	assert.Check(t, releasesKey != rootKeyB)
   130  }
   131  
   132  func TestAddStageSigners(t *testing.T) {
   133  	skip.If(t, runtime.GOOS == "windows", "FIXME: not supported currently")
   134  	tmpDir, err := ioutil.TempDir("", "notary-test-")
   135  	assert.NilError(t, err)
   136  	defer os.RemoveAll(tmpDir)
   137  
   138  	notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
   139  	assert.NilError(t, err)
   140  
   141  	// stage targets/user
   142  	userRole := data.RoleName("targets/user")
   143  	userKey := data.NewPublicKey("algoA", []byte("a"))
   144  	err = addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey})
   145  	assert.NilError(t, err)
   146  	// check the changelist for four total changes: two on targets/releases and two on targets/user
   147  	cl, err := notaryRepo.GetChangelist()
   148  	assert.NilError(t, err)
   149  	changeList := cl.List()
   150  	assert.Check(t, is.Len(changeList, 4))
   151  	// ordering is deterministic:
   152  
   153  	// first change is for targets/user key creation
   154  	newSignerKeyChange := changeList[0]
   155  	expectedJSON, err := json.Marshal(&changelist.TUFDelegation{
   156  		NewThreshold: notary.MinThreshold,
   157  		AddKeys:      data.KeyList([]data.PublicKey{userKey}),
   158  	})
   159  	assert.NilError(t, err)
   160  	expectedChange := changelist.NewTUFChange(
   161  		changelist.ActionCreate,
   162  		userRole,
   163  		changelist.TypeTargetsDelegation,
   164  		"", // no path for delegations
   165  		expectedJSON,
   166  	)
   167  	assert.Check(t, is.DeepEqual(expectedChange, newSignerKeyChange))
   168  
   169  	// second change is for targets/user getting all paths
   170  	newSignerPathsChange := changeList[1]
   171  	expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
   172  		AddPaths: []string{""},
   173  	})
   174  	assert.NilError(t, err)
   175  	expectedChange = changelist.NewTUFChange(
   176  		changelist.ActionCreate,
   177  		userRole,
   178  		changelist.TypeTargetsDelegation,
   179  		"", // no path for delegations
   180  		expectedJSON,
   181  	)
   182  	assert.Check(t, is.DeepEqual(expectedChange, newSignerPathsChange))
   183  
   184  	releasesRole := data.RoleName("targets/releases")
   185  
   186  	// third change is for targets/releases key creation
   187  	releasesKeyChange := changeList[2]
   188  	expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
   189  		NewThreshold: notary.MinThreshold,
   190  		AddKeys:      data.KeyList([]data.PublicKey{userKey}),
   191  	})
   192  	assert.NilError(t, err)
   193  	expectedChange = changelist.NewTUFChange(
   194  		changelist.ActionCreate,
   195  		releasesRole,
   196  		changelist.TypeTargetsDelegation,
   197  		"", // no path for delegations
   198  		expectedJSON,
   199  	)
   200  	assert.Check(t, is.DeepEqual(expectedChange, releasesKeyChange))
   201  
   202  	// fourth change is for targets/releases getting all paths
   203  	releasesPathsChange := changeList[3]
   204  	expectedJSON, err = json.Marshal(&changelist.TUFDelegation{
   205  		AddPaths: []string{""},
   206  	})
   207  	assert.NilError(t, err)
   208  	expectedChange = changelist.NewTUFChange(
   209  		changelist.ActionCreate,
   210  		releasesRole,
   211  		changelist.TypeTargetsDelegation,
   212  		"", // no path for delegations
   213  		expectedJSON,
   214  	)
   215  	assert.Check(t, is.DeepEqual(expectedChange, releasesPathsChange))
   216  }
   217  
   218  func TestGetSignedManifestHashAndSize(t *testing.T) {
   219  	tmpDir, err := ioutil.TempDir("", "notary-test-")
   220  	assert.NilError(t, err)
   221  	defer os.RemoveAll(tmpDir)
   222  
   223  	notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
   224  	assert.NilError(t, err)
   225  	target := &client.Target{}
   226  	target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, "test")
   227  	assert.Error(t, err, "client is offline")
   228  }
   229  
   230  func TestGetReleasedTargetHashAndSize(t *testing.T) {
   231  	oneReleasedTgt := []client.TargetSignedStruct{}
   232  	// make and append 3 non-released signatures on the "unreleased" target
   233  	unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
   234  	for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
   235  		oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
   236  	}
   237  	_, _, err := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased")
   238  	assert.Error(t, err, "No valid trust data for unreleased")
   239  	releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
   240  	oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
   241  	hash, _, _ := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased")
   242  	assert.Check(t, is.DeepEqual(data.Hashes{notary.SHA256: []byte("released-hash")}, hash))
   243  
   244  }
   245  
   246  func TestCreateTarget(t *testing.T) {
   247  	tmpDir, err := ioutil.TempDir("", "notary-test-")
   248  	assert.NilError(t, err)
   249  	defer os.RemoveAll(tmpDir)
   250  
   251  	notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
   252  	assert.NilError(t, err)
   253  	_, err = createTarget(notaryRepo, "")
   254  	assert.Error(t, err, "No tag specified")
   255  	_, err = createTarget(notaryRepo, "1")
   256  	assert.Error(t, err, "client is offline")
   257  }
   258  
   259  func TestGetExistingSignatureInfoForReleasedTag(t *testing.T) {
   260  	tmpDir, err := ioutil.TempDir("", "notary-test-")
   261  	assert.NilError(t, err)
   262  	defer os.RemoveAll(tmpDir)
   263  
   264  	notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
   265  	assert.NilError(t, err)
   266  	_, err = getExistingSignatureInfoForReleasedTag(notaryRepo, "test")
   267  	assert.Error(t, err, "client is offline")
   268  }
   269  
   270  func TestPrettyPrintExistingSignatureInfo(t *testing.T) {
   271  	buf := bytes.NewBuffer(nil)
   272  	signers := []string{"Bob", "Alice", "Carol"}
   273  	existingSig := trustTagRow{trustTagKey{"tagName", "abc123"}, signers}
   274  	prettyPrintExistingSignatureInfo(buf, existingSig)
   275  
   276  	assert.Check(t, is.Contains(buf.String(), "Existing signatures for tag tagName digest abc123 from:\nAlice, Bob, Carol"))
   277  }
   278  
   279  func TestSignCommandChangeListIsCleanedOnError(t *testing.T) {
   280  	tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
   281  	assert.NilError(t, err)
   282  	defer os.RemoveAll(tmpDir)
   283  
   284  	config.SetDir(tmpDir)
   285  	cli := test.NewFakeCli(&fakeClient{})
   286  	cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository)
   287  	cmd := newSignCommand(cli)
   288  	cmd.SetArgs([]string{"ubuntu:latest"})
   289  	cmd.SetOut(ioutil.Discard)
   290  
   291  	err = cmd.Execute()
   292  	assert.Assert(t, err != nil)
   293  
   294  	notaryRepo, err := client.NewFileCachedRepository(tmpDir, "docker.io/library/ubuntu", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
   295  	assert.NilError(t, err)
   296  	cl, err := notaryRepo.GetChangelist()
   297  	assert.NilError(t, err)
   298  	assert.Check(t, is.Equal(len(cl.List()), 0))
   299  }
   300  
   301  func TestSignCommandLocalFlag(t *testing.T) {
   302  	cli := test.NewFakeCli(&fakeClient{})
   303  	cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository)
   304  	cmd := newSignCommand(cli)
   305  	cmd.SetArgs([]string{"--local", "reg-name.io/image:red"})
   306  	cmd.SetOut(ioutil.Discard)
   307  	assert.ErrorContains(t, cmd.Execute(), "error contacting notary server: dial tcp: lookup reg-name.io")
   308  
   309  }