Repository: theupdateframework/go-tuf
Pull request #170 (2021-10-22 19:55:32)

Add digest to targets metadata directly
Allows users of go-tuf to sign oci images or other non-local targets by directly providing the hash and length of these artifacts. Solves #165

cc @ethan-lowman-dd @trishankatdatadog @sudo-bmitch

joshuagl: The build is failing in `go fmt`, which only runs on Linux (i.e. Ubuntu), see https://github.com/theupdateframework/go-tuf/blob/fc0190d925a5f43942c51c5e519b8fce3b025610/.github/workflows/build.yml#L24

If I run `go fmt` locally on a clone of this branch it reformats repo_test.go thus:
```diff
diff --git a/repo_test.go b/repo_test.go
index bc55210..c764162 100644
--- a/repo_test.go
+++ b/repo_test.go
@@ -1684,7 +1684,7 @@ func (rs *RepoSuite) TestBadAddOrUpdateSignatures(c *C) {
        checkSigIDs("root.json")
 }

-func (rs *RepoSuite) TestSignDigest(c *C){
+func (rs *RepoSuite) TestSignDigest(c *C) {
        files := map[string][]byte{"foo.txt": []byte("foo")}
        local := MemoryStore(make(map[string]json.RawMessage), files)
        r, err := NewRepo(local)
@@ -1708,7 +1708,7 @@ func (rs *RepoSuite) TestSignDigest(c *C){
        c.Assert(err, IsNil)

        targets, err := r.targets()
-               c.Assert(err, IsNil)
+       c.Assert(err, IsNil)
        c.Assert(targets.Targets["sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"].FileMeta.Length, Equals, size)
        c.Assert(targets.Targets["sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"].FileMeta.Hashes["sha256"], DeepEquals, hex_digest_bytes)
```
mnm678: Thank for the review @ethan-lowman-dd! 
coveralls: ## Pull Request Test Coverage Report for [Build 1517465013](https://coveralls.io/builds/44609805)

* **39** of **52**   **(75.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.1%**) to **68.6%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [client/client.go](https://coveralls.io/builds/44609805/source?filename=client%2Fclient.go#L907) | 15 | 19 | 78.95%
| [repo.go](https://coveralls.io/builds/44609805/source?filename=repo.go#L704) | 24 | 33 | 72.73%
<!-- | **Total:** | **39** | **52** | **75.0%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/44609805/badge)](https://coveralls.io/builds/44609805) |
| :-- | --: |
| Change from base [Build 1462851572](https://coveralls.io/builds/44269411): |  0.1% |
| Covered Lines: | 2058 |
| Relevant Lines: | 3000 |

---
##### 💛  - [Coveralls](https://coveralls.io)

mnm678: I added the client verification and fixed the test failures, so I think this is ready for another review @ethan-lowman-dd or @trishankatdatadog 
trishankatdatadog: > I added the client verification and fixed the test failures, so I think this is ready for another review @ethan-lowman-dd or @trishankatdatadog

Awesome! Ethan, would you please help? 🙂 
mnm678: ping @trishankatdatadog you're marked as requesting changes. Do the latest commits address those?
trishankatdatadog: > ping @trishankatdatadog you're marked as requesting changes. Do the latest commits address those?

Will review today during our meeting. Thanks!
mnm678: Thanks @asraa. I addressed your comments and rebased for some of the delegations pr changes.

Patch:
diff --git a/client/client.go b/client/client.go
index 0459a1d..1ecff4d 100644
--- a/client/client.go
+++ b/client/client.go
@@ -2,6 +2,7 @@ package client
 
 import (
 	"bytes"
+	"encoding/hex"
 	"encoding/json"
 	"io"
 	"io/ioutil"
@@ -841,6 +842,29 @@ func (c *Client) Download(name string, dest Destination) (err error) {
 	return nil
 }
 
+func (c *Client) VerifyDigest(digest string, digestAlg string, length int64, path string) error {
+	localMeta, ok := c.targets[path]
+	if !ok {
+		return ErrUnknownTarget{Name: path, SnapshotVersion: c.snapshotVer}
+	}
+
+	actual := data.FileMeta{Length: length, Hashes: make(data.Hashes, 1)}
+	var err error
+	actual.Hashes[digestAlg], err = hex.DecodeString(digest)
+	if err != nil {
+		return err
+	}
+
+	if err := util.TargetFileMetaEqual(data.TargetFileMeta{FileMeta: actual}, localMeta); err != nil {
+		if e, ok := err.(util.ErrWrongLength); ok {
+			return ErrWrongSize{path, e.Actual, e.Expected}
+		}
+		return ErrDownloadFailed{path, err}
+	}
+
+	return nil
+}
+
 // Target returns the target metadata for a specific target if it
 // exists, searching from top-level level targets then through
 // all delegations. If it does not, ErrNotFound will be returned.
diff --git a/repo.go b/repo.go
index e4cec16..6055410 100644
--- a/repo.go
+++ b/repo.go
@@ -2,6 +2,7 @@ package tuf
 
 import (
 	"bytes"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -703,6 +704,34 @@ func (r *Repo) AddTargets(paths []string, custom json.RawMessage) error {
 	return r.AddTargetsWithExpires(paths, custom, data.DefaultExpires("targets"))
 }
 
+func (r *Repo) AddTargetsWithDigest(digest string, digestAlg string, length int64, path string, custom json.RawMessage) error {
+	expires := data.DefaultExpires("targets")
+
+	// TODO: support delegated targets
+	t, err := r.topLevelTargets()
+	if err != nil {
+		return err
+	}
+
+	meta := data.FileMeta{Length: length, Hashes: make(data.Hashes, 1)}
+	meta.Hashes[digestAlg], err = hex.DecodeString(digest)
+	if err != nil {
+		return err
+	}
+
+	// If custom is provided, set custom, otherwise maintain existing custom
+	// metadata
+	if len(custom) > 0 {
+		meta.Custom = &custom
+	} else if t, ok := t.Targets[path]; ok {
+		meta.Custom = t.Custom
+	}
+
+	t.Targets[path] = data.TargetFileMeta{FileMeta: meta}
+
+	return r.writeTargetWithExpires(t, expires)
+}
+
 func (r *Repo) AddTargetWithExpires(path string, custom json.RawMessage, expires time.Time) error {
 	return r.AddTargetsWithExpires([]string{path}, custom, expires)
 }
@@ -742,12 +771,16 @@ func (r *Repo) AddTargetsWithExpires(paths []string, custom json.RawMessage, exp
 	}); err != nil {
 		return err
 	}
+	return r.writeTargetWithExpires(t, expires)
+}
+
+func (r *Repo) writeTargetWithExpires(t *data.Targets, expires time.Time) error {
 	t.Expires = expires.Round(time.Second)
 	if !r.local.FileIsStaged("targets.json") {
 		t.Version++
 	}
 
-	err = r.setTopLevelMeta("targets.json", t)
+	err := r.setTopLevelMeta("targets.json", t)
 	if err == nil {
 		fmt.Println("Added/staged targets:")
 		for k := range t.Targets {


Test patch:
diff --git a/client/client_test.go b/client/client_test.go
index 5cbaf44..c0be244 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -1178,3 +1178,21 @@ func generateRepoFS(c *C, dir string, files map[string][]byte, consistentSnapsho
 	c.Assert(repo.Commit(), IsNil)
 	return repo
 }
+
+func (s *ClientSuite) TestVerifyDigest(c *C) {
+	digest := "sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"
+	hash := "bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"
+	size := int64(42)
+
+	c.Assert(s.repo.AddTargetsWithDigest(hash, "sha256", size, digest, nil), IsNil)
+	c.Assert(s.repo.Snapshot(), IsNil)
+	c.Assert(s.repo.Timestamp(), IsNil)
+	c.Assert(s.repo.Commit(), IsNil)
+	s.syncRemote(c)
+
+	client := s.newClient(c)
+	_, err := client.Update()
+	c.Assert(err, IsNil)
+
+	c.Assert(client.VerifyDigest(hash, "sha256", size, digest), IsNil)
+}
diff --git a/repo_test.go b/repo_test.go
index f378aa8..fbecd61 100644
--- a/repo_test.go
+++ b/repo_test.go
@@ -3,6 +3,7 @@ package tuf
 import (
 	"crypto"
 	"crypto/rand"
+	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -1795,3 +1796,34 @@ func (rs *RepoSuite) TestBadAddOrUpdateSignatures(c *C) {
 	}
 	checkSigIDs("root.json")
 }
+
+func (rs *RepoSuite) TestSignDigest(c *C) {
+	files := map[string][]byte{"foo.txt": []byte("foo")}
+	local := MemoryStore(make(map[string]json.RawMessage), files)
+	r, err := NewRepo(local)
+	c.Assert(err, IsNil)
+
+	genKey(c, r, "root")
+	genKey(c, r, "targets")
+	genKey(c, r, "snapshot")
+	genKey(c, r, "timestamp")
+
+	digest := "sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"
+	hash := "bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"
+	size := int64(42)
+
+	c.Assert(r.AddTargetsWithDigest(hash, "sha256", size, digest, nil), IsNil)
+	c.Assert(r.Snapshot(), IsNil)
+	c.Assert(r.Timestamp(), IsNil)
+	c.Assert(r.Commit(), IsNil)
+
+	digest_bytes, err := hex.DecodeString("bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c")
+	hex_digest_bytes := data.HexBytes(digest_bytes)
+	c.Assert(err, IsNil)
+
+	targets, err := r.topLevelTargets()
+	c.Assert(err, IsNil)
+	c.Assert(targets.Targets["sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"].FileMeta.Length, Equals, size)
+	c.Assert(targets.Targets["sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"].FileMeta.Hashes["sha256"], DeepEquals, hex_digest_bytes)
+
+}