Repository: theupdateframework/go-tuf (go)

Issue:
Unable to initialize client without fetching from remote
Before calling `client.Update` or `client.UpdateRoots`, it is expected that the client has been initialized with `client.Init`. [`Init`](https://github.com/theupdateframework/go-tuf/blob/master/client/client.go#L114) uses a set of root keys, pulls the latest `root.json`, verifies it with those keys, and saves it locally.

The issue is there's no guarantee that the latest root can be verified using the set of root keys. For example, let's say a project is shipped with the first `root.json`. Before a new root signing, one key is rotated. After 5 signings, all keys used to sign the remote `root.json` will have been rotated. `Init` will fail, and there's no other way to directly initialize the client.

The proper way to initialize a client would be to initialize the client with a local root, then walk and verify the chain using `Update` or `UpdateRoots`. I'd like to propose that `Init` take the local `root.json` to initialize the database, and not make any calls to the remote to fetch the latest `root.json`.

Another option would be for `Init` to take the set of root keys and threshold, its current API, and call `UpdateRoots` as part of initialization. I would prefer that this not be the default, for those who don't want to initialize a client with the latest root.

cc @asraa 

Pull request:
refactor!: rename "InitLocal" to "Init"
Straightforward find/replace:

    find . -name '*.go' | xargs sed -i 's/InitLocal/Init/g'

BREAKING CHANGE: the method signature of `Init()` has changed since the
last release to take in the raw `root.json` metadata.

Signed-off-by: Zachary Newman <z@znewman.net>

Please fill in the fields below to submit a pull request.  The more information that is provided, the better.

Fixes #208
Release Notes: the method signature of `Init()` has changed since the
last release to take in the raw `root.json` metadata.

**Types of changes**:
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [X] Breaking change (fix or feature that would cause existing functionality to not work as expected)

**Description of the changes being introduced by the pull request**: rename "InitLocal" to "Init"

**Please verify and check that the pull request fulfills the following requirements**:

- [X] Tests have been added for the bug fix or new feature
- [X] Docs have been added for the bug fix or new feature


Patch:
diff --git a/client/client.go b/client/client.go
index 42756f8..188d8a8 100644
--- a/client/client.go
+++ b/client/client.go
@@ -106,13 +106,13 @@ func NewClient(local LocalStore, remote RemoteStore) *Client {
 	}
 }
 
-// InitLocal initializes a local repository from root metadata.
+// Init initializes a local repository from root metadata.
 //
 // The root's keys are extracted from the root and saved in local storage.
 // Root expiration is not checked.
 // It is expected that rootJSON was securely distributed with the software
 // being updated.
-func (c *Client) InitLocal(rootJSON []byte) error {
+func (c *Client) Init(rootJSON []byte) error {
 	err := c.loadAndVerifyRootMeta(rootJSON, true /*ignoreExpiredCheck*/)
 	if err != nil {
 		return err
diff --git a/cmd/tuf-client/init.go b/cmd/tuf-client/init.go
index f043c0b..b3a0f6a 100644
--- a/cmd/tuf-client/init.go
+++ b/cmd/tuf-client/init.go
@@ -35,5 +35,5 @@ func cmdInit(args *docopt.Args, client *tuf.Client) error {
 	if err != nil {
 		return err
 	}
-	return client.InitLocal(bytes)
+	return client.Init(bytes)
 }


Test patch:
diff --git a/client/client_test.go b/client/client_test.go
index c23a966..f312f84 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -185,7 +185,7 @@ func (s *ClientSuite) rootMeta(c *C) []byte {
 func (s *ClientSuite) newClient(c *C) *Client {
 	s.local = MemoryLocalStore()
 	client := NewClient(s.local, s.remote)
-	c.Assert(client.InitLocal(s.rootMeta(c)), IsNil)
+	c.Assert(client.Init(s.rootMeta(c)), IsNil)
 	return client
 }
 
@@ -244,26 +244,7 @@ func (s *ClientSuite) assertErrExpired(c *C, err error, file string) {
 	c.Assert(expiredErr.Expired.Unix(), Equals, s.expiredTime.Round(time.Second).Unix())
 }
 
-func (s *ClientSuite) TestInit(c *C) {
-	client := NewClient(MemoryLocalStore(), s.remote)
-
-	// check invalid json
-	c.Assert(client.InitLocal(make([]byte, 0)), NotNil)
-	rootJson := `{ "signatures": [], "signed": {"version": "wrongtype"}, "spec_version": "1.0.0", "version": 1}`
-	err := client.InitLocal([]byte(rootJson))
-	c.Assert(err.Error(), Matches, "json: cannot unmarshal string.*")
-
-	// check Update() returns ErrNoRootKeys when uninitialized
-	_, err = client.Update()
-	c.Assert(err, Equals, ErrNoRootKeys)
-
-	// check Update() does not return ErrNoRootKeys after initialization
-	c.Assert(client.InitLocal(s.rootMeta(c)), IsNil)
-	_, err = client.Update()
-	c.Assert(err, IsNil)
-}
-
-func (s *ClientSuite) TestInitLocalAllowsExpired(c *C) {
+func (s *ClientSuite) TestInitAllowsExpired(c *C) {
 	s.genKeyExpired(c, "targets")
 	c.Assert(s.repo.Snapshot(), IsNil)
 	c.Assert(s.repo.Timestamp(), IsNil)
@@ -273,11 +254,11 @@ func (s *ClientSuite) TestInitLocalAllowsExpired(c *C) {
 	bytes, err := io.ReadAll(s.remote.meta["root.json"])
 	c.Assert(err, IsNil)
 	s.withMetaExpired(func() {
-		c.Assert(client.InitLocal(bytes), IsNil)
+		c.Assert(client.Init(bytes), IsNil)
 	})
 }
 
-func (s *ClientSuite) TestInitLocal(c *C) {
+func (s *ClientSuite) TestInit(c *C) {
 	client := NewClient(MemoryLocalStore(), s.remote)
 	bytes, err := io.ReadAll(s.remote.meta["root.json"])
 	c.Assert(err, IsNil)
@@ -290,7 +271,7 @@ func (s *ClientSuite) TestInitLocal(c *C) {
 	_, err = client.Update()
 	c.Assert(err, Equals, ErrNoRootKeys)
 
-	// check InitLocal() returns ErrInvalid when the root's signature is
+	// check Init() returns ErrInvalid when the root's signature is
 	// invalid
 	// modify root and marshal without regenerating signatures
 	root.Version = root.Version + 1
@@ -299,10 +280,10 @@ func (s *ClientSuite) TestInitLocal(c *C) {
 	dataSigned.Signed = rootBytes
 	dataBytes, err := json.Marshal(dataSigned)
 	c.Assert(err, IsNil)
-	c.Assert(client.InitLocal(dataBytes), Equals, verify.ErrInvalid)
+	c.Assert(client.Init(dataBytes), Equals, verify.ErrInvalid)
 
 	// check Update() does not return ErrNoRootKeys after initialization
-	c.Assert(client.InitLocal(bytes), IsNil)
+	c.Assert(client.Init(bytes), IsNil)
 	_, err = client.Update()
 	c.Assert(err, IsNil)
 }
@@ -1042,7 +1023,7 @@ func (s *ClientSuite) TestUpdateHTTP(c *C) {
 		c.Assert(err, IsNil)
 		rootJsonBytes, err := json.Marshal(rootMeta)
 		c.Assert(err, IsNil)
-		c.Assert(client.InitLocal(rootJsonBytes), IsNil)
+		c.Assert(client.Init(rootJsonBytes), IsNil)
 
 		// check update is ok
 		targets, err := client.Update()
diff --git a/client/delegations_test.go b/client/delegations_test.go
index c7303ac..250e7b7 100644
--- a/client/delegations_test.go
+++ b/client/delegations_test.go
@@ -275,7 +275,7 @@ func initTestDelegationClient(t *testing.T, dirPrefix string) (*Client, func() e
 	c := NewClient(MemoryLocalStore(), remote)
 	rawFile, err := ioutil.ReadFile(initialStateDir + "/" + "root.json")
 	assert.Nil(t, err)
-	assert.Nil(t, c.InitLocal(rawFile))
+	assert.Nil(t, c.Init(rawFile))
 	files, err := ioutil.ReadDir(initialStateDir)
 	assert.Nil(t, err)
 
diff --git a/client/interop_test.go b/client/interop_test.go
index 8e4becb..15ece0d 100644
--- a/client/interop_test.go
+++ b/client/interop_test.go
@@ -153,7 +153,7 @@ func (t *testCase) runStep(c *C, stepName string) {
 	c.Assert(err, IsNil)
 	rootJsonBytes, err := io.ReadAll(ioReader)
 	c.Assert(err, IsNil)
-	c.Assert(client.InitLocal(rootJsonBytes), IsNil)
+	c.Assert(client.Init(rootJsonBytes), IsNil)
 
 	// check update returns the correct updated targets
 	files, err := client.Update()
diff --git a/client/python_interop/python_interop_test.go b/client/python_interop/python_interop_test.go
index 7a4abdc..e2e2a91 100644
--- a/client/python_interop/python_interop_test.go
+++ b/client/python_interop/python_interop_test.go
@@ -59,7 +59,7 @@ func (InteropSuite) TestGoClientPythonGenerated(c *C) {
 		client := client.NewClient(client.MemoryLocalStore(), remote)
 		rootJSON, err := ioutil.ReadFile(filepath.Join(testDataDir, dir, "repository", "metadata", "root.json"))
 		c.Assert(err, IsNil)
-		c.Assert(client.InitLocal(rootJSON), IsNil)
+		c.Assert(client.Init(rootJSON), IsNil)
 
 		// check update returns the correct updated targets
 		files, err := client.Update()