diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f25ffa80..56186b3f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,14 @@ Please fill in the fields below to submit a pull request. The more information that is provided, the better. Fixes # + Release Notes: **Types of changes**: - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected). **Please ensure that your PR title** is a [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) breaking change (with a `!`, as in `feat!: change foo`). **Description of the changes being introduced by the pull request**: diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index c283a6be..9c16b8e8 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -13,6 +13,6 @@ jobs: runs-on: "ubuntu-latest" steps: - name: - uses: amannn/action-semantic-pull-request@91682d013dea3ff257520b9b68c9cb93ced4fe9b + uses: amannn/action-semantic-pull-request@01d5fd8a8ebb9aafe902c40c53f0f4744f7381eb env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6de89310..295923ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,8 @@ on: tags: - "v*" name: CI +permissions: + contents: write jobs: tests: uses: ./.github/workflows/tests.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00018f61..54ee03f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,14 +16,14 @@ jobs: with: fetch-depth: 0 - name: Get Go version - uses: arnested/go-version-action@6d27c5921683f55415260f7a285d330ddb146fa2 + uses: arnested/go-version-action@b556f8d91b644164318c709d28b9083eaf0c064d id: go-version - name: Set up Go - uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 with: go-version: ${{ steps.go-version.outputs.minimal }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@ff11ca24a9b39f2d36796d1fbd7a4e39c182630a + uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 with: distribution: goreleaser version: "v1.7.0" diff --git a/.github/workflows/specification-version-check.yml b/.github/workflows/specification-version-check.yml index 1c4834f8..f128207d 100644 --- a/.github/workflows/specification-version-check.yml +++ b/.github/workflows/specification-version-check.yml @@ -11,4 +11,4 @@ jobs: issues: write uses: theupdateframework/specification/.github/workflows/check-latest-spec-version.yml@master with: - tuf-version: "v1.0.29" # Should be updated to the according version either manually or extracted automatically as how it's done in python-tuf + tuf-version: "v1.0.31" # Should be updated to the according version either manually or extracted automatically as how it's done in python-tuf diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a805655a..007387ca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,11 +9,12 @@ jobs: matrix: ${{ steps.versions.outputs.matrix }} steps: - uses: actions/checkout@v3 - - uses: arnested/go-version-action@6d27c5921683f55415260f7a285d330ddb146fa2 + - uses: arnested/go-version-action@b556f8d91b644164318c709d28b9083eaf0c064d id: versions run: strategy: + fail-fast: false # Keep running if one leg fails. matrix: os: [ubuntu-latest, macos-latest, windows-latest] go-version: ${{ fromJSON(needs.get-go-versions.outputs.matrix) }} @@ -24,12 +25,12 @@ jobs: uses: actions/checkout@v3 - name: Setup - Go ${{ matrix.go-version }} - uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 with: go-version: ${{ matrix.go-version }} - name: Setup - Python - uses: actions/setup-python@b55428b1882923874294fa556849718a1d7f2ca5 + uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 with: python-version: "3.10" cache: "pip" @@ -58,12 +59,12 @@ jobs: runs-on: ${{ matrix.os }} needs: get-go-versions steps: - - uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f + - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 with: go-version: ${{ matrix.go-version }} - uses: actions/checkout@v3 - name: golangci-lint - uses: golangci/golangci-lint-action@537aa1903e5d359d0b27dbc19ddd22c5087f3fbc + uses: golangci/golangci-lint-action@0ad9a0988b3973e851ab0a07adf248ec2e100376 with: - version: v1.49.0 - args: --timeout 3m + version: v1.49 + args: --timeout 5m --verbose diff --git a/.golangci.yml b/.golangci.yml index 6e8bf3c8..570c05d6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,3 +13,4 @@ linters: - gosimple - unused - typecheck + - forbidigo diff --git a/.goreleaser/tuf.yml b/.goreleaser/tuf.yml index eaa45c77..4662f273 100644 --- a/.goreleaser/tuf.yml +++ b/.goreleaser/tuf.yml @@ -23,7 +23,7 @@ changelog: use: github groups: - title: "Breaking changes" - regexp: "^.*BREAKING CHANGE[(\\w)]*:+.*$" + regexp: "^.*(?:BREAKING CHANGE)|![(\\w)]*:+.*$" order: 0 - title: Features regexp: "^.*feat[(\\w)]*:+.*$" diff --git a/README.md b/README.md index b1a4b6ea..125978c1 100644 --- a/README.md +++ b/README.md @@ -603,6 +603,39 @@ $ tree . └── staged ``` +#### Adding a new root key + +Copy `staged/root.json` to the root box and generate a new root key on the root box: + +```bash +$ tuf gen-key root +$ tuf sign root.json +``` + +Copy `staged/root.json` from the root box and commit: + +```bash +$ tuf commit +``` + +#### Rotating root key(s) + +Copy `staged/root.json` to the root box to do the rotation, where `abcd` is the keyid of the key that is being replaced: + +```bash +$ tuf gen-key root +$ tuf revoke-key root abcd +$ tuf sign root.json +``` + +Note that `revoke-key` removes the old key from `root.json`, but the key remains in the `keys/` directory on the root box as it is needed to sign the next `root.json`. After this signing is done, the old key may be removed from `keys/`. Any number of keys may be added or revoked during this step, but ensure that at least a threshold of valid keys remain. + +Copy `staged/root.json` from the root box to commit: + +```bash +$ tuf commit +``` + ## Client For the client package, see https://godoc.org/github.com/theupdateframework/go-tuf/client. diff --git a/client/client.go b/client/client.go index 17ddc980..978ebf1d 100644 --- a/client/client.go +++ b/client/client.go @@ -4,9 +4,11 @@ import ( "bytes" "encoding/hex" "encoding/json" + "fmt" "io" "github.com/theupdateframework/go-tuf/data" + "github.com/theupdateframework/go-tuf/internal/roles" "github.com/theupdateframework/go-tuf/util" "github.com/theupdateframework/go-tuf/verify" ) @@ -143,7 +145,13 @@ func (c *Client) Update() (data.TargetFiles, error) { } // 5.4.(2,3 and 4) - Verify timestamp against various attacks // Returns the extracted snapshot metadata - snapshotMeta, err := c.decodeTimestamp(timestampJSON) + snapshotMeta, sameTimestampVersion, err := c.decodeTimestamp(timestampJSON) + if sameTimestampVersion { + // The new timestamp.json file had the same version; we don't need to + // update, so bail early. + return c.targets, nil + } + if err != nil { return nil, err } @@ -393,8 +401,8 @@ func (c *Client) getLocalMeta() error { } } + snapshot := &data.Snapshot{} if snapshotJSON, ok := meta["snapshot.json"]; ok { - snapshot := &data.Snapshot{} if err := c.db.UnmarshalTrusted(snapshotJSON, snapshot, "snapshot"); err != nil { loadFailed = true retErr = err @@ -417,6 +425,35 @@ func (c *Client) getLocalMeta() error { c.loadTargets(targets.Targets) } } + + if loadFailed { + // If any of the metadata failed to be verified, return the reason for that failure + // and fail fast before delegated targets + return retErr + } + + // verifiedDelegatedTargets is a set of verified delegated targets + verifiedDelegatedTargets := make(map[string]bool) + for fileName := range meta { + if !verifiedDelegatedTargets[fileName] && roles.IsDelegatedTargetsManifest(fileName) { + if delegationPath, err := c.getDelegationPathFromRaw(snapshot, meta[fileName]); err != nil { + loadFailed = true + retErr = err + } else { + // Every delegated targets in the path has been verified + // as a side effect of getDelegationPathFromRaw + for _, key := range delegationPath { + fileName := fmt.Sprintf("%s.json", key) + verifiedDelegatedTargets[fileName] = true + } + } + } + } + + for fileName := range verifiedDelegatedTargets { + c.localMeta[fileName] = meta[fileName] + } + if loadFailed { // If any of the metadata failed to be verified, return the reason for that failure return retErr @@ -424,6 +461,47 @@ func (c *Client) getLocalMeta() error { return nil } +// getDelegationPathFromRaw verifies a delegated targets against +// a given snapshot and returns an error if it's invalid +// +// Delegation must have targets to get a path, else an empty list +// will be returned: this is because the delegation iterator is leveraged. +// +// Concrete example: +// targets +// └── a.json +//   └── b.json +//      └── c.json +//        └── target_file.txt +// +// If you try to use that function on "a.json" or "b.json", it'll return an empty list +// with no error, as neither of them declare a target file +// On the other hand, if you use that function on "c.json", it'll return & verify +// [c.json, b.json, a.json]. Running that function on every delegated targets +// guarantees that if a delegated targets is in the path of a target file, then it will +// appear at least once in the result +func (c *Client) getDelegationPathFromRaw(snapshot *data.Snapshot, delegatedTargetsJSON json.RawMessage) ([]string, error) { + // unmarshal the delegated targets first without verifying as + // we need at least one targets file name to leverage the + // getTargetFileMetaDelegationPath method + s := &data.Signed{} + if err := json.Unmarshal(delegatedTargetsJSON, s); err != nil { + return nil, err + } + targets := &data.Targets{} + if err := json.Unmarshal(s.Signed, targets); err != nil { + return nil, err + } + for targetPath := range targets.Targets { + _, resp, err := c.getTargetFileMetaDelegationPath(targetPath, snapshot) + // We only need to test one targets file: + // - If it is valid, it means the delegated targets has been validated + // - If it is not, the delegated targets isn't valid + return resp, err + } + return nil, nil +} + // loadAndVerifyLocalRootMeta decodes and verifies root metadata from // local storage and loads the top-level keys. This method first clears // the DB for top-level keys and then loads the new keys. @@ -740,22 +818,31 @@ func (c *Client) decodeTargets(b json.RawMessage) (data.TargetFiles, error) { } // decodeTimestamp decodes and verifies timestamp metadata, and returns the -// new snapshot file meta. -func (c *Client) decodeTimestamp(b json.RawMessage) (data.TimestampFileMeta, error) { +// new snapshot file meta and signals whether the update should be aborted early +// (the new timestamp has the same version as the old one, so there's no need to +// complete the update). +func (c *Client) decodeTimestamp(b json.RawMessage) (data.TimestampFileMeta, bool, error) { timestamp := &data.Timestamp{} + if err := c.db.Unmarshal(b, timestamp, "timestamp", c.timestampVer); err != nil { - return data.TimestampFileMeta{}, ErrDecodeFailed{"timestamp.json", err} + return data.TimestampFileMeta{}, false, ErrDecodeFailed{"timestamp.json", err} + } + // 5.4.3.1 - Check for timestamp rollback attack + // We already checked for timestamp.Version < c.timestampVer in the Unmarshal call above. + // Here, we're checking for version equality, which indicates that we can abandon this update. + if timestamp.Version == c.timestampVer { + return data.TimestampFileMeta{}, true, nil } // 5.4.3.2 - Check for snapshot rollback attack // Verify that the current snapshot meta version is less than or equal to the new one if timestamp.Meta["snapshot.json"].Version < c.snapshotVer { - return data.TimestampFileMeta{}, verify.ErrLowVersion{Actual: timestamp.Meta["snapshot.json"].Version, Current: c.snapshotVer} + return data.TimestampFileMeta{}, false, verify.ErrLowVersion{Actual: timestamp.Meta["snapshot.json"].Version, Current: c.snapshotVer} } - // At this point we can trust the new timestamp and the snaphost version it refers to + // At this point we can trust the new timestamp and the snapshot version it refers to // so we can update the client's trusted versions and proceed with persisting the new timestamp c.timestampVer = timestamp.Version c.snapshotVer = timestamp.Meta["snapshot.json"].Version - return timestamp.Meta["snapshot.json"], nil + return timestamp.Meta["snapshot.json"], false, nil } // hasMetaFromSnapshot checks whether local metadata has the given meta @@ -792,6 +879,8 @@ type Destination interface { // - The target does not exist in any targets // - Metadata cannot be generated for the downloaded data // - Generated metadata does not match local metadata for the given file +// - Size of the download does not match if the reported size is known and +// incorrect func (c *Client) Download(name string, dest Destination) (err error) { // delete dest if there is an error defer func() { diff --git a/client/client_test.go b/client/client_test.go index db8c3272..f4dbd885 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,6 +2,7 @@ package client import ( "bytes" + "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" @@ -31,15 +32,19 @@ import ( func Test(t *testing.T) { TestingT(t) } type ClientSuite struct { - store tuf.LocalStore - repo *tuf.Repo - local LocalStore - remote *fakeRemoteStore - expiredTime time.Time - keyIDs map[string][]string + store tuf.LocalStore + repo *tuf.Repo + local LocalStore + remote RemoteStore + expiredTime time.Time + keyIDs map[string][]string + useFileStore bool + // Only used with FileStore + tmpDir string } -var _ = Suite(&ClientSuite{}) +var _ = Suite(&ClientSuite{useFileStore: false}) +var _ = Suite(&ClientSuite{useFileStore: true}) func newFakeRemoteStore() *fakeRemoteStore { return &fakeRemoteStore{ @@ -69,6 +74,66 @@ func (f *fakeRemoteStore) get(name string, store map[string]*fakeFile) (io.ReadC return file, file.size, nil } +// These are helper methods for manipulating the internals of the Stores +// because the set/delete methods are not part of the Interface, we need to +// switch on the underlying implementation. +// Also readMeta method is convenience for ease of testing. +func (s *ClientSuite) setRemoteMeta(path string, data []byte) error { + switch impl := s.remote.(type) { + case *fakeRemoteStore: + impl.meta[path] = newFakeFile(data) + return nil + case *FileRemoteStore: + return impl.addMeta(path, data) + default: + return fmt.Errorf("non-supoprted RemoteStore, got %+v", impl) + } +} + +func (s *ClientSuite) setRemoteTarget(path string, data []byte) error { + switch impl := s.remote.(type) { + case *fakeRemoteStore: + impl.targets[path] = newFakeFile(data) + return nil + case *FileRemoteStore: + return impl.addTarget(path, data) + default: + return fmt.Errorf("non-supoprted RemoteStore, got %+v", impl) + } +} + +func (s *ClientSuite) deleteMeta(path string) error { + switch impl := s.remote.(type) { + case *fakeRemoteStore: + delete(impl.meta, path) + return nil + case *FileRemoteStore: + return impl.deleteMeta(path) + default: + return fmt.Errorf("non-supported RemoteStore, got %+v", impl) + } +} + +func (s *ClientSuite) deleteTarget(path string) error { + switch impl := s.remote.(type) { + case *fakeRemoteStore: + delete(impl.targets, path) + return nil + case *FileRemoteStore: + return impl.deleteTarget(path) + default: + return fmt.Errorf("non-supported RemoteStore, got %+v", impl) + } +} + +func (s *ClientSuite) readMeta(name string) ([]byte, error) { + stream, _, err := s.remote.GetMeta(name) + if err != nil { + return nil, err + } + return io.ReadAll(stream) +} + func newFakeFile(b []byte) *fakeFile { return &fakeFile{buf: bytes.NewReader(b), size: int64(len(b))} } @@ -118,15 +183,28 @@ func (s *ClientSuite) SetUpTest(c *C) { c.Assert(s.repo.Commit(), IsNil) // create a remote store containing valid repo files - s.remote = newFakeRemoteStore() + if s.useFileStore { + s.remote, s.tmpDir, err = newTestFileStoreFS() + if err != nil { + c.Fatalf("failed to create new FileStore: %v", err) + } + } else { + s.remote = newFakeRemoteStore() + } s.syncRemote(c) for path, data := range targetFiles { - s.remote.targets[path] = newFakeFile(data) + s.setRemoteTarget(path, data) } s.expiredTime = time.Now().Add(time.Hour) } +func (s *ClientSuite) TearDownTest(c *C) { + if s.tmpDir != "" { + rmrf(s.tmpDir, c.Logf) + } +} + func (s *ClientSuite) genKey(c *C, role string) []string { ids, err := s.repo.GenKey(role) c.Assert(err, IsNil) @@ -163,7 +241,9 @@ func (s *ClientSuite) syncRemote(c *C) { meta, err := s.store.GetMeta() c.Assert(err, IsNil) for name, data := range meta { - s.remote.meta[name] = newFakeFile(data) + if err := s.setRemoteMeta(name, data); err != nil { + panic(fmt.Sprintf("setMetadata failed: %v", err)) + } } } @@ -252,7 +332,7 @@ func (s *ClientSuite) TestInitAllowsExpired(c *C) { c.Assert(s.repo.Commit(), IsNil) s.syncRemote(c) client := NewClient(MemoryLocalStore(), s.remote) - bytes, err := io.ReadAll(s.remote.meta["root.json"]) + bytes, err := s.readMeta("root.json") c.Assert(err, IsNil) s.withMetaExpired(func() { c.Assert(client.Init(bytes), IsNil) @@ -261,7 +341,7 @@ func (s *ClientSuite) TestInitAllowsExpired(c *C) { func (s *ClientSuite) TestInit(c *C) { client := NewClient(MemoryLocalStore(), s.remote) - bytes, err := io.ReadAll(s.remote.meta["root.json"]) + bytes, err := s.readMeta("root.json") c.Assert(err, IsNil) dataSigned := &data.Signed{} c.Assert(json.Unmarshal(bytes, dataSigned), IsNil) @@ -272,7 +352,7 @@ func (s *ClientSuite) TestInit(c *C) { _, err = client.Update() c.Assert(err, Equals, ErrNoRootKeys) - // check Init() returns ErrInvalid when the root's signature is + // check Init() returns ErrRoleThreshold when the root's signature is // invalid // modify root and marshal without regenerating signatures root.Version = root.Version + 1 @@ -281,7 +361,8 @@ func (s *ClientSuite) TestInit(c *C) { dataSigned.Signed = rootBytes dataBytes, err := json.Marshal(dataSigned) c.Assert(err, IsNil) - c.Assert(client.Init(dataBytes), Equals, verify.ErrInvalid) + c.Assert(client.Init(dataBytes), Equals, verify.ErrRoleThreshold{ + Expected: 1, Actual: 0}) // check Update() does not return ErrNoRootKeys after initialization c.Assert(client.Init(bytes), IsNil) @@ -289,6 +370,29 @@ func (s *ClientSuite) TestInit(c *C) { c.Assert(err, IsNil) } +// This is a regression test for https://github.com/theupdateframework/go-tuf/issues/370 +// where a single invalid signature resulted in an early return. +// Instead, the client should have continued and counted the number +// of valid signatures, ignoring the incorrect one. +func (s *ClientSuite) TestExtraRootSignaturesOnInit(c *C) { + client := NewClient(MemoryLocalStore(), s.remote) + bytes, err := s.readMeta("root.json") + c.Assert(err, IsNil) + dataSigned := &data.Signed{} + c.Assert(json.Unmarshal(bytes, dataSigned), IsNil) + + // check Init() succeeds when an extra invalid signature was + // added to the root. + dataSigned.Signatures = append(dataSigned.Signatures, + data.Signature{ + KeyID: dataSigned.Signatures[0].KeyID, + Signature: make([]byte, ed25519.SignatureSize), + }) + dataBytes, err := json.Marshal(dataSigned) + c.Assert(err, IsNil) + c.Assert(client.Init(dataBytes), IsNil) +} + func (s *ClientSuite) TestFirstUpdate(c *C) { files, err := s.newClient(c).Update() c.Assert(err, IsNil) @@ -299,11 +403,11 @@ func (s *ClientSuite) TestFirstUpdate(c *C) { func (s *ClientSuite) TestMissingRemoteMetadata(c *C) { client := s.newClient(c) - delete(s.remote.meta, "targets.json") + s.deleteMeta("targets.json") _, err := client.Update() c.Assert(err, Equals, ErrMissingRemoteMetadata{"targets.json"}) - delete(s.remote.meta, "timestamp.json") + s.deleteMeta("timestamp.json") _, err = client.Update() c.Assert(err, Equals, ErrMissingRemoteMetadata{"timestamp.json"}) } @@ -376,6 +480,44 @@ func (s *ClientSuite) TestNewRoot(c *C) { } } +// This is a regression test for https://github.com/theupdateframework/go-tuf/issues/370 +// where a single invalid signature resulted in an early return. +// Instead, the client should have continued and counted the number +// of valid signatures, ignoring the incorrect one. +func (s *ClientSuite) TestExtraSignaturesOnRootUpdate(c *C) { + client := s.newClient(c) + + // Add an extra root key to update the root to a new version. + s.genKey(c, "root") + // update metadata + c.Assert(s.repo.Sign("targets.json"), IsNil) + c.Assert(s.repo.Snapshot(), IsNil) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) + s.syncRemote(c) + + // Add an extra signature to the new remote root. + bytes, err := s.readMeta("root.json") + c.Assert(err, IsNil) + dataSigned := &data.Signed{} + c.Assert(json.Unmarshal(bytes, dataSigned), IsNil) + dataSigned.Signatures = append(dataSigned.Signatures, + data.Signature{ + KeyID: dataSigned.Signatures[0].KeyID, + Signature: make([]byte, ed25519.SignatureSize), + }) + dataBytes, err := json.Marshal(dataSigned) + c.Assert(err, IsNil) + s.setRemoteMeta("root.json", dataBytes) + s.setRemoteMeta("2.root.json", dataBytes) + + // check Update() succeeds when an extra invalid signature was + // added to the root. + _, err = client.Update() + c.Assert(err, IsNil) + c.Assert(client.rootVer, Equals, int64(2)) +} + // startTUFRepoServer starts a HTTP server to serve a TUF Repo. func startTUFRepoServer(baseDir string, relPath string) (net.Listener, error) { serverDir := filepath.Join(baseDir, relPath) @@ -438,9 +580,11 @@ func (s *ClientSuite) TestUpdateRoots(c *C) { // Fails updating root from version 1 to version 3 when versions 1 and 3 are expired but version 2 is not expired. {"testdata/Published3Times_keyrotated_latestrootexpired", ErrDecodeFailed{File: "root.json", Err: verify.ErrExpired{}}, map[string]int64{"root": 2, "timestamp": 1, "snapshot": 1, "targets": 1}}, // Fails updating root from version 1 to version 2 when old root 1 did not sign off on it (nth root didn't sign off n+1). - {"testdata/Published2Times_keyrotated_invalidOldRootSignature", errors.New("tuf: signature verification failed"), map[string]int64{}}, + // TODO(asraa): This testcase should have revoked the old key! + // https://github.com/theupdateframework/go-tuf/issues/417 + {"testdata/Published2Times_keyrotated_invalidOldRootSignature", nil, map[string]int64{}}, // Fails updating root from version 1 to version 2 when the new root 2 did not sign itself (n+1th root didn't sign off n+1) - {"testdata/Published2Times_keyrotated_invalidNewRootSignature", errors.New("tuf: signature verification failed"), map[string]int64{}}, + {"testdata/Published2Times_keyrotated_invalidNewRootSignature", verify.ErrRoleThreshold{Expected: 1, Actual: 0}, map[string]int64{}}, // Fails updating root to 2.root.json when the value of the version field inside it is 1 (rollback attack prevention). {"testdata/Published1Time_backwardRootVersion", verify.ErrWrongVersion(verify.ErrWrongVersion{Given: 1, Expected: 2}), map[string]int64{}}, // Fails updating root to 2.root.json when the value of the version field inside it is 3 (rollforward attack prevention). @@ -774,7 +918,7 @@ func (s *ClientSuite) TestLocalExpired(c *C) { } func (s *ClientSuite) TestTimestampTooLarge(c *C) { - s.remote.meta["timestamp.json"] = newFakeFile(make([]byte, defaultTimestampDownloadLimit+1)) + s.setRemoteMeta("timestamp.json", make([]byte, defaultTimestampDownloadLimit+1)) _, err := s.newClient(c).Update() c.Assert(err, Equals, ErrMetaTooLarge{"timestamp.json", defaultTimestampDownloadLimit + 1, defaultTimestampDownloadLimit}) } @@ -896,12 +1040,13 @@ func (s *ClientSuite) TestUpdateMixAndMatchAttack(c *C) { c.Assert(s.repo.AddTargetWithExpires("foo.txt", nil, expires), IsNil) c.Assert(s.repo.Snapshot(), IsNil) c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) s.syncRemote(c) client := s.updatedClient(c) // grab the remote targets.json - oldTargets, ok := s.remote.meta["targets.json"] - if !ok { + oldTargets, err := s.readMeta("targets.json") + if err != nil { c.Fatal("missing remote targets.json") } @@ -909,23 +1054,24 @@ func (s *ClientSuite) TestUpdateMixAndMatchAttack(c *C) { c.Assert(s.repo.AddTargetWithExpires("bar.txt", nil, expires), IsNil) c.Assert(s.repo.Snapshot(), IsNil) c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) s.syncRemote(c) - newTargets, ok := s.remote.meta["targets.json"] - if !ok { + newTargets, err := s.readMeta("targets.json") + if err != nil { c.Fatal("missing remote targets.json") } - s.remote.meta["targets.json"] = oldTargets + s.setRemoteMeta("targets.json", oldTargets) // check update returns ErrWrongSize for targets.json - _, err := client.Update() - c.Assert(err, DeepEquals, ErrWrongSize{"targets.json", oldTargets.size, newTargets.size}) + _, err = client.Update() + c.Assert(err, DeepEquals, ErrWrongSize{"targets.json", int64(len(oldTargets)), int64(len(newTargets))}) // do the same but keep the size the same c.Assert(s.repo.RemoveTargetWithExpires("foo.txt", expires), IsNil) c.Assert(s.repo.Snapshot(), IsNil) c.Assert(s.repo.Timestamp(), IsNil) s.syncRemote(c) - s.remote.meta["targets.json"] = oldTargets + s.setRemoteMeta("targets.json", oldTargets) // check update returns ErrWrongHash _, err = client.Update() @@ -936,8 +1082,8 @@ func (s *ClientSuite) TestUpdateReplayAttack(c *C) { client := s.updatedClient(c) // grab the remote timestamp.json - oldTimestamp, ok := s.remote.meta["timestamp.json"] - if !ok { + oldTimestamp, err := s.readMeta("timestamp.json") + if err != nil { c.Fatal("missing remote timestamp.json") } @@ -946,12 +1092,12 @@ func (s *ClientSuite) TestUpdateReplayAttack(c *C) { c.Assert(version > 0, Equals, true) c.Assert(s.repo.Timestamp(), IsNil) s.syncRemote(c) - _, err := client.Update() + _, err = client.Update() c.Assert(err, IsNil) c.Assert(client.timestampVer > version, Equals, true) // replace remote timestamp.json with the old one - s.remote.meta["timestamp.json"] = oldTimestamp + s.setRemoteMeta("timestamp.json", oldTimestamp) // check update returns ErrLowVersion _, err = client.Update() @@ -964,6 +1110,41 @@ func (s *ClientSuite) TestUpdateReplayAttack(c *C) { }) } +func (s *ClientSuite) TestUpdateForkTimestamp(c *C) { + client := s.updatedClient(c) + + // grab the remote timestamp.json + oldTimestamp, err := s.readMeta("timestamp.json") + if err != nil { + c.Fatal("missing remote timestamp.json") + } + + // generate a new timestamp and sync with the client + version := client.timestampVer + c.Assert(version > 0, Equals, true) + c.Assert(s.repo.Timestamp(), IsNil) + s.syncRemote(c) + _, err = client.Update() + c.Assert(err, IsNil) + newVersion := client.timestampVer + c.Assert(newVersion > version, Equals, true) + + // generate a new, different timestamp with the *same version* + s.setRemoteMeta("timestamp.json", oldTimestamp) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(client.timestampVer, Equals, newVersion) // double-check: same version? + s.syncRemote(c) + + oldMeta, err := client.local.GetMeta() + c.Assert(err, IsNil) + _, err = client.Update() + c.Assert(err, IsNil) // no error: the targets.json version didn't change, so there was no update! + // Client shouldn't update! + newMeta, err := client.local.GetMeta() + c.Assert(err, IsNil) + c.Assert(oldMeta, DeepEquals, newMeta) +} + func (s *ClientSuite) TestUpdateTamperedTargets(c *C) { client := s.newClient(c) @@ -998,6 +1179,7 @@ func (s *ClientSuite) TestUpdateTamperedTargets(c *C) { c.Assert(err, IsNil) s.store.SetMeta("targets.json", tamperedJSON) s.store.Commit(false, nil, nil) + c.Assert(s.repo.Timestamp(), IsNil) // unless timestamp changes, the client doesn't even look at "targets.json" s.syncRemote(c) _, err = client.Update() c.Assert(err, DeepEquals, ErrWrongSize{"targets.json", int64(len(tamperedJSON)), int64(len(targetsJSON))}) @@ -1041,6 +1223,125 @@ func (s *ClientSuite) TestUpdateHTTP(c *C) { } } +// TestRollbackSnapshot tests a rollback version of snapshot. +func (s *ClientSuite) TestRollbackSnapshot(c *C) { + client := s.updatedClient(c) + + // generate a new snapshot & timestamp v2 and sync with the client + version := client.snapshotVer + c.Assert(version > 0, Equals, true) + c.Assert(s.repo.Snapshot(), IsNil) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) + s.syncRemote(c) + _, err := client.Update() + c.Assert(err, IsNil) + c.Assert(client.snapshotVer > version, Equals, true) + + // replace remote snapshot.json with old version and timestamp again. + s.repo.SetSnapshotVersion(version) + c.Assert(s.repo.Snapshot(), IsNil) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) + s.syncRemote(c) + + // check update returns ErrLowVersion + _, err = client.Update() + + c.Assert(err, DeepEquals, verify.ErrLowVersion{ + Actual: version, + Current: client.snapshotVer, + }) +} + +func (s *ClientSuite) TestRollbackTopLevelTargets(c *C) { + client := s.updatedClient(c) + + // generate a new targets and sync with the client + version := client.targetsVer + c.Assert(version > 0, Equals, true) + s.addRemoteTarget(c, "bar.txt") + _, err := client.Update() + c.Assert(err, IsNil) + c.Assert(client.targetsVer > version, Equals, true) + + // replace remote snapshot.json with old version and timestamp again. + s.repo.SetTargetsVersion(version) + c.Assert(s.repo.Snapshot(), IsNil) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) + s.syncRemote(c) + + // check update returns ErrLowVersion + _, err = client.Update() + c.Assert(err, DeepEquals, verify.ErrLowVersion{ + Actual: version, + Current: client.targetsVer, + }) +} + +func (s *ClientSuite) TestRollbackDelegatedTargets(c *C) { + client := s.updatedClient(c) + // add a delegation + signer, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + role := data.DelegatedRole{ + Name: "role", + KeyIDs: signer.PublicData().IDs(), + Paths: []string{"bar.txt", "baz.txt"}, + Threshold: 1, + } + s.store.SaveSigner("role", signer) + s.repo.AddDelegatedRole("targets", role, []*data.PublicKey{signer.PublicData()}) + s.repo.AddTargetToPreferredRole("bar.txt", nil, "role") + c.Assert(s.repo.Snapshot(), IsNil) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) + s.syncRemote(c) + + // save v1 delegation + meta, err := s.store.GetMeta() + c.Assert(err, IsNil) + oldRole, ok := meta["role.json"] + if !ok { + c.Fatal("missing role.json") + } + // update client and verify download delegated target + _, err = client.Update() + c.Assert(err, IsNil) + var dest testDestination + c.Assert(client.Download("bar.txt", &dest), IsNil) + + // update delegation to v2 + s.repo.AddTargetToPreferredRole("baz.txt", nil, "role") + c.Assert(s.repo.Snapshot(), IsNil) + c.Assert(s.repo.Timestamp(), IsNil) + c.Assert(s.repo.Commit(), IsNil) + s.syncRemote(c) + + // update client and verify download v2 delegated target + _, err = client.Update() + c.Assert(err, IsNil) + c.Assert(dest.Delete(), IsNil) + c.Assert(client.Download("baz.txt", &dest), IsNil) + + // rollback role.json version. + c.Assert(s.store.SetMeta("role.json", oldRole), IsNil) + repo, err := tuf.NewRepo(s.store) + c.Assert(err, IsNil) + c.Assert(repo.Snapshot(), IsNil) + c.Assert(repo.Timestamp(), IsNil) + c.Assert(repo.Commit(), IsNil) + s.syncRemote(c) + + // check update returns ErrLowVersion + _, err = client.Update() + c.Assert(err, DeepEquals, verify.ErrLowVersion{ + Actual: 1, + Current: 2, + }) +} + type testDestination struct { bytes.Buffer deleted bool @@ -1060,7 +1361,7 @@ func (s *ClientSuite) TestDownloadUnknownTarget(c *C) { func (s *ClientSuite) TestDownloadNoExist(c *C) { client := s.updatedClient(c) - delete(s.remote.targets, "foo.txt") + s.deleteTarget("foo.txt") var dest testDestination c.Assert(client.Download("foo.txt", &dest), Equals, ErrNotFound{"foo.txt"}) c.Assert(dest.deleted, Equals, true) @@ -1079,29 +1380,24 @@ func (s *ClientSuite) TestDownloadOK(c *C) { func (s *ClientSuite) TestDownloadWrongSize(c *C) { client := s.updatedClient(c) - remoteFile := &fakeFile{buf: bytes.NewReader([]byte("wrong-size")), size: 10} - s.remote.targets["foo.txt"] = remoteFile + // Update with a file that's incorrect size. + s.setRemoteTarget("foo.txt", []byte("wrong-size")) var dest testDestination c.Assert(client.Download("foo.txt", &dest), DeepEquals, ErrWrongSize{"foo.txt", 10, 3}) - c.Assert(remoteFile.bytesRead, Equals, 0) c.Assert(dest.deleted, Equals, true) } func (s *ClientSuite) TestDownloadTargetTooLong(c *C) { client := s.updatedClient(c) - remoteFile := s.remote.targets["foo.txt"] - remoteFile.buf = bytes.NewReader([]byte("foo-ooo")) + s.setRemoteTarget("foo.txt", []byte("foo-ooo")) var dest testDestination - c.Assert(client.Download("foo.txt", &dest), IsNil) - c.Assert(remoteFile.bytesRead, Equals, 3) - c.Assert(dest.deleted, Equals, false) - c.Assert(dest.String(), Equals, "foo") + c.Assert(client.Download("foo.txt", &dest), DeepEquals, ErrWrongSize{"foo.txt", 7, 3}) + c.Assert(dest.deleted, Equals, true) } func (s *ClientSuite) TestDownloadTargetTooShort(c *C) { client := s.updatedClient(c) - remoteFile := s.remote.targets["foo.txt"] - remoteFile.buf = bytes.NewReader([]byte("fo")) + s.setRemoteTarget("foo.txt", []byte("fo")) var dest testDestination c.Assert(client.Download("foo.txt", &dest), DeepEquals, ErrWrongSize{"foo.txt", 2, 3}) c.Assert(dest.deleted, Equals, true) @@ -1109,8 +1405,7 @@ func (s *ClientSuite) TestDownloadTargetTooShort(c *C) { func (s *ClientSuite) TestDownloadTargetCorruptData(c *C) { client := s.updatedClient(c) - remoteFile := s.remote.targets["foo.txt"] - remoteFile.buf = bytes.NewReader([]byte("corrupt")) + s.setRemoteTarget("foo.txt", []byte("ooo")) var dest testDestination assertWrongHash(c, client.Download("foo.txt", &dest)) c.Assert(dest.deleted, Equals, true) diff --git a/client/delegations.go b/client/delegations.go index de3e6647..cc0fc482 100644 --- a/client/delegations.go +++ b/client/delegations.go @@ -8,13 +8,23 @@ import ( // getTargetFileMeta searches for a verified TargetFileMeta matching a target // Requires a local snapshot to be loaded and is locked to the snapshot versions. -// Searches through delegated targets following TUF spec 1.0.19 section 5.6. func (c *Client) getTargetFileMeta(target string) (data.TargetFileMeta, error) { snapshot, err := c.loadLocalSnapshot() if err != nil { return data.TargetFileMeta{}, err } + targetFileMeta, _, err := c.getTargetFileMetaDelegationPath(target, snapshot) + if err != nil { + return data.TargetFileMeta{}, err + } + return targetFileMeta, nil +} + +// getTargetFileMetaDelegationPath searches for a verified TargetFileMeta matching a target +// Requires snapshot to be passed and is locked to that specific snapshot versions. +// Searches through delegated targets following TUF spec 1.0.19 section 5.6. +func (c *Client) getTargetFileMetaDelegationPath(target string, snapshot *data.Snapshot) (data.TargetFileMeta, []string, error) { // delegationsIterator covers 5.6.7 // - pre-order depth-first search starting with the top targets // - filter delegations with paths or path_hash_prefixes matching searched target @@ -22,50 +32,75 @@ func (c *Client) getTargetFileMeta(target string) (data.TargetFileMeta, error) { // - 5.6.7.2 terminations delegations, err := targets.NewDelegationsIterator(target, c.db) if err != nil { - return data.TargetFileMeta{}, err + return data.TargetFileMeta{}, nil, err } + targetFileMeta := data.TargetFileMeta{} + delegationRole := "" + for i := 0; i < c.MaxDelegations; i++ { d, ok := delegations.Next() if !ok { - return data.TargetFileMeta{}, ErrUnknownTarget{target, snapshot.Version} + return data.TargetFileMeta{}, nil, ErrUnknownTarget{target, snapshot.Version} } // covers 5.6.{1,2,3,4,5,6} targets, err := c.loadDelegatedTargets(snapshot, d.Delegatee.Name, d.DB) if err != nil { - return data.TargetFileMeta{}, err + return data.TargetFileMeta{}, nil, err } // stop when the searched TargetFileMeta is found if m, ok := targets.Targets[target]; ok { - return m, nil + delegationRole = d.Delegatee.Name + targetFileMeta = m + break } if targets.Delegations != nil { delegationsDB, err := verify.NewDBFromDelegations(targets.Delegations) if err != nil { - return data.TargetFileMeta{}, err + return data.TargetFileMeta{}, nil, err } err = delegations.Add(targets.Delegations.Roles, d.Delegatee.Name, delegationsDB) if err != nil { - return data.TargetFileMeta{}, err + return data.TargetFileMeta{}, nil, err } } } - return data.TargetFileMeta{}, ErrMaxDelegations{ + if len(delegationRole) > 0 { + return targetFileMeta, buildPath(delegations.Parent, delegationRole, ""), nil + } + + return data.TargetFileMeta{}, nil, ErrMaxDelegations{ Target: target, MaxDelegations: c.MaxDelegations, SnapshotVersion: snapshot.Version, } } +func buildPath(parent func(string) string, start string, end string) []string { + if start == end { + return nil + } + + path := []string{start} + current := start + for { + current = parent(current) + if current == end { + break + } + path = append(path, current) + } + return path +} + func (c *Client) loadLocalSnapshot() (*data.Snapshot, error) { if err := c.getLocalMeta(); err != nil { return nil, err } - rawS, ok := c.localMeta["snapshot.json"] if !ok { return nil, ErrNoLocalSnapshot diff --git a/client/delegations_test.go b/client/delegations_test.go index 47fdc229..6d5a7767 100644 --- a/client/delegations_test.go +++ b/client/delegations_test.go @@ -226,17 +226,27 @@ func TestPersistedMeta(t *testing.T) { p, err := c.local.GetMeta() assert.Nil(t, err) persisted := copyStore(p) + persistedLocal := copyStore(c.localMeta) // trim non targets metas for _, notTargets := range []string{"root.json", "snapshot.json", "timestamp.json"} { delete(persisted, notTargets) + delete(persistedLocal, notTargets) } for _, targets := range tt.targets { + // Test local store storedVersion, err := versionOfStoredTargets(targets.name, persisted) assert.Equal(t, targets.version, storedVersion) assert.Nil(t, err) delete(persisted, targets.name) + + // Test localMeta + storedVersion, err = versionOfStoredTargets(targets.name, persistedLocal) + assert.Equal(t, targets.version, storedVersion) + assert.Nil(t, err) + delete(persistedLocal, targets.name) } assert.Empty(t, persisted) + assert.Empty(t, persistedLocal) }) } } diff --git a/client/file_store.go b/client/file_store.go new file mode 100644 index 00000000..520bbe73 --- /dev/null +++ b/client/file_store.go @@ -0,0 +1,90 @@ +package client + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" +) + +// FileRemoteStore provides a RemoteStore interface compatible +// implementation that can be used where the RemoteStore is backed by a +// fs.FS. This is useful for example in air-gapped environments where there's no +// possibility to make outbound network connections. +// By having this be a fs.FS instead of directories allows the repository to +// be backed by something that's not persisted to disk. +func NewFileRemoteStore(fsys fs.FS, targetDir string) (*FileRemoteStore, error) { + if fsys == nil { + return nil, errors.New("nil fs.FS") + } + t := targetDir + if t == "" { + t = "targets" + } + // Make sure directory exists + d, err := fsys.Open(t) + if err != nil { + return nil, fmt.Errorf("failed to open targets directory %s: %w", t, err) + } + fi, err := d.Stat() + if err != nil { + return nil, fmt.Errorf("failed to stat targets directory %s: %w", t, err) + } + if !fi.IsDir() { + return nil, fmt.Errorf("targets directory not a directory %s", t) + } + + fsysT, err := fs.Sub(fsys, t) + if err != nil { + return nil, fmt.Errorf("failed to open targets directory %s: %w", t, err) + } + return &FileRemoteStore{fsys: fsys, targetDir: fsysT}, nil +} + +type FileRemoteStore struct { + // Meta directory fs + fsys fs.FS + // Target directory fs. + targetDir fs.FS + // In order to be able to make write operations (create, delete) we can't + // use fs.FS for it (it's read only), so we have to know the underlying + // directory that add/delete test methods can use. This is only necessary + // for testing purposes. + testDir string +} + +func (f *FileRemoteStore) GetMeta(name string) (io.ReadCloser, int64, error) { + rc, b, err := f.get(f.fsys, name) + return handleErrors(name, rc, b, err) +} + +func (f *FileRemoteStore) GetTarget(name string) (io.ReadCloser, int64, error) { + rc, b, err := f.get(f.targetDir, name) + return handleErrors(name, rc, b, err) +} + +func (f *FileRemoteStore) get(fsys fs.FS, s string) (io.ReadCloser, int64, error) { + if !fs.ValidPath(s) { + return nil, 0, fmt.Errorf("invalid path %s", s) + } + + b, err := fs.ReadFile(fsys, s) + if err != nil { + return nil, -1, err + } + return io.NopCloser(bytes.NewReader(b)), int64(len(b)), nil +} + +// handleErrors converts NotFound errors to something that TUF knows how to +// handle properly. For example, when looking for n+1 root files, this is a +// signal that it will stop looking. +func handleErrors(name string, rc io.ReadCloser, b int64, err error) (io.ReadCloser, int64, error) { + if err == nil { + return rc, b, err + } + if errors.Is(err, fs.ErrNotExist) { + return rc, b, ErrNotFound{name} + } + return rc, b, err +} diff --git a/client/file_store_test.go b/client/file_store_test.go new file mode 100644 index 00000000..95e02e38 --- /dev/null +++ b/client/file_store_test.go @@ -0,0 +1,197 @@ +package client + +import ( + "bytes" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +const targetsDir = "targets" + +func TestCreates(t *testing.T) { + runningWindows := false + if runtime.GOOS == "windows" { + runningWindows = true + } + tmpDir := t.TempDir() + defer os.RemoveAll(tmpDir) + dir := filepath.Join(tmpDir, "repository") + os.Mkdir(dir, os.ModePerm) + os.Mkdir(filepath.Join(dir, "targets"), os.ModePerm) + if !runningWindows { + targetDirThatIsFile := filepath.Join(dir, "targets-that-isfile") + f, err := os.Create(targetDirThatIsFile) + if err != nil { + t.Fatalf("failed to create file: %s: %v", targetDirThatIsFile, err) + } + defer f.Close() + } + t.Cleanup(func() { rmrf(dir, t.Logf) }) + t.Cleanup(func() { rmrf(tmpDir, t.Logf) }) + + tests := []struct { + name string + fsys fs.FS + td string + wantErr string + doNotRunOnWindows bool + }{{ + name: "nil, error", + wantErr: "nil fs.FS", + }, { + name: "missing targets directory", + fsys: os.DirFS(dir), + td: "targets-not-there", + wantErr: "failed to open targets directory targets-not-there", + }, { + name: "targets directory is not a file", + fsys: os.DirFS(dir), + td: "targets-that-isfile", + wantErr: "targets directory not a directory targets-that-isfile", + doNotRunOnWindows: true, + }, { + name: "works, explicit targets", + fsys: os.DirFS(dir), + td: "targets", + }, { + name: "works, explicit targets", + fsys: os.DirFS(dir), + td: "targets", + }} + + for _, tc := range tests { + if tc.doNotRunOnWindows { + t.Skip("Can't figure out how to make this work on windows") + } + _, err := NewFileRemoteStore(tc.fsys, tc.td) + if tc.wantErr != "" && err == nil { + t.Errorf("%q wanted error %s, got none", tc.name, tc.wantErr) + } else if tc.wantErr == "" && err != nil { + t.Errorf("%q did not want error, got: %v", tc.name, err) + } else if err != nil && !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("%q wanted error %s but got: %s", tc.name, tc.wantErr, err) + } + } +} + +func TestBasicOps(t *testing.T) { + metas := map[string][]byte{ + "root.json": []byte("root"), + "snapshot.json": []byte("snapshot"), + "timestamp": []byte("timestamp"), + } + + fsys, dir, err := newTestFileStoreFS() + if err != nil { + t.Fatalf("Failed to create test FileStore") + } + t.Cleanup(func() { rmrf(dir, t.Logf) }) + defer os.RemoveAll(dir) + + // Add targets and metas and check them. + for k, v := range targetFiles { + if err := fsys.addTarget(k, v); err != nil { + t.Errorf("failed to add target %s: %v", k, err) + } + rc, size, err := fsys.GetTarget(k) + if err != nil { + t.Errorf("failed to GetTarget %s: %v", k, err) + } + if size != int64(len(v)) { + t.Errorf("unexpected size returned for GetTarget: %s want %d got %d", k, len(v), size) + } + got, err := io.ReadAll(rc) + if err != nil { + t.Errorf("failed to ReadAll returned ReacCloser %s: %v", k, err) + } + if !bytes.Equal(v, got) { + t.Errorf("Read unexpected bytes, want: %s got: %s", string(k), string(got)) + } + } + for k, v := range metas { + if err := fsys.addMeta(k, v); err != nil { + t.Errorf("failed to add meta %s %v", k, err) + } + rc, size, err := fsys.GetMeta(k) + if err != nil { + t.Errorf("failed to GetMeta %s: %v", k, err) + } + if size != int64(len(v)) { + t.Errorf("unexpected size returned for GetMeta: %s want %d got %d", k, len(v), size) + } + got, err := io.ReadAll(rc) + if err != nil { + t.Errorf("failed to ReadAll returned ReacCloser %s: %v", k, err) + } + if !bytes.Equal(v, got) { + t.Errorf("Read unexpected bytes, want: %s got: %s", string(k), string(got)) + } + } +} + +// Test helper methods +func (f *FileRemoteStore) addMeta(name string, data []byte) error { + return os.WriteFile(filepath.Join(f.testDir, name), data, os.ModePerm) +} + +func (f *FileRemoteStore) addTarget(name string, data []byte) error { + fname := filepath.Join(f.testDir, targetsDir, name) + err := os.WriteFile(fname, data, os.ModePerm) + return err +} + +func (f *FileRemoteStore) deleteMeta(name string) error { + return os.Remove(filepath.Join(f.testDir, name)) +} + +func (f *FileRemoteStore) deleteTarget(name string) error { + return os.Remove(filepath.Join(f.testDir, targetsDir, name)) +} + +func newTestFileStoreFS() (*FileRemoteStore, string, error) { + tmpDir := os.TempDir() + tufDir := filepath.Join(tmpDir, "tuf-file-store-test") + // Clean it in case there is cruft left around + os.RemoveAll(tufDir) + os.Mkdir(tufDir, os.ModePerm) + os.Mkdir(filepath.Join(tufDir, targetsDir), os.ModePerm) + fs, err := NewFileRemoteStore(os.DirFS(tufDir), targetsDir) + fs.testDir = tufDir + return fs, tufDir, err +} + +// goes through a dir and removes everything. This is to work around: +// https://github.com/golang/go/issues/51442 +func rmrf(dir string, logger func(string, ...interface{})) { + if dir == "" { + logger("cowardly refusing to remove a not fully specified fir") + return + } + logger("Removing %s", dir) + d, err := os.Open(dir) + if err != nil { + logger("Failed to open %s: %v", dir, err) + return + } + defer d.Close() + // -1 means give me everything, we don't have that many entries, so + // fine here. + names, err := d.Readdirnames(-1) + if err != nil { + logger("Failed to ReaddirNames %s: %v", dir, err) + return + } + for _, name := range names { + toRemove := filepath.Join(dir, name) + err = os.RemoveAll(toRemove) + if err != nil { + logger("Failed to RemoveAll %s: %v", toRemove, err) + // Do not want to fail here, just keep doing the best we can + } + } +} diff --git a/client/python_interop/python_interop_test.go b/client/python_interop/python_interop_test.go index cd968f93..b906e230 100644 --- a/client/python_interop/python_interop_test.go +++ b/client/python_interop/python_interop_test.go @@ -43,7 +43,7 @@ func (InteropSuite) TestGoClientPythonGenerated(c *C) { // start file server cwd, err := os.Getwd() c.Assert(err, IsNil) - testDataDir := filepath.Join(cwd, "testdata", "python-tuf-v1.0.0") + testDataDir := filepath.Join(cwd, "testdata", "python-tuf-v2.0.0") addr, cleanup := startFileServer(c, testDataDir) defer cleanup() @@ -85,7 +85,8 @@ func (InteropSuite) TestGoClientPythonGenerated(c *C) { } } -func generateRepoFS(c *C, dir string, files map[string][]byte, consistentSnapshot bool) *tuf.Repo { +func generateRepoFS(c *C, dir string, files map[string][]byte, + consistentSnapshot bool) *tuf.Repo { repo, err := tuf.NewRepo(tuf.FileSystemStore(dir, nil)) c.Assert(err, IsNil) if !consistentSnapshot { @@ -107,6 +108,12 @@ func generateRepoFS(c *C, dir string, files map[string][]byte, consistentSnapsho return repo } +func refreshRepo(c *C, repo *tuf.Repo) { + c.Assert(repo.Snapshot(), IsNil) + c.Assert(repo.Timestamp(), IsNil) + c.Assert(repo.Commit(), IsNil) +} + func (InteropSuite) TestPythonClientGoGenerated(c *C) { // clone the Python client if necessary cwd, err := os.Getwd() @@ -138,7 +145,66 @@ func (InteropSuite) TestPythonClientGoGenerated(c *C) { c.Assert(os.WriteFile(filepath.Join(currDir, "root.json"), rootJSON, 0644), IsNil) args := []string{ - filepath.Join(cwd, "testdata", "python-tuf-v1.0.0", "client.py"), + filepath.Join(cwd, "testdata", "python-tuf-v2.0.0", "client.py"), + "--repo=http://" + addr + "/" + name, + } + for path := range files { + args = append(args, path) + } + + // run Python client update + cmd := exec.Command("python", args...) + cmd.Dir = clientDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + c.Assert(cmd.Run(), IsNil) + + // check the target files got downloaded + for path, expected := range files { + actual, err := os.ReadFile(filepath.Join(clientDir, "tuftargets", url.QueryEscape(path))) + c.Assert(err, IsNil) + c.Assert(actual, DeepEquals, expected) + } + } +} + +// This is a regression test for issue +// https://github.com/theupdateframework/go-tuf/issues/402 +func (InteropSuite) TestPythonClientGoGeneratedNullDelegations(c *C) { + // clone the Python client if necessary + cwd, err := os.Getwd() + c.Assert(err, IsNil) + + files := map[string][]byte{ + "foo.txt": []byte("foo"), + "bar/baz.txt": []byte("baz"), + } + + for _, consistentSnapshot := range []bool{false, true} { + // generate repository + tmp := c.MkDir() + // start file server + addr, cleanup := startFileServer(c, tmp) + defer cleanup() + name := fmt.Sprintf("consistent-snapshot-delegations-%t", consistentSnapshot) + dir := filepath.Join(tmp, name) + repo := generateRepoFS(c, dir, files, consistentSnapshot) + // "Reset" top-level targets delegations and re-sign + c.Assert(repo.ResetTargetsDelegations("targets"), IsNil) + refreshRepo(c, repo) + + // create initial files for Python client + clientDir := filepath.Join(dir, "client") + currDir := filepath.Join(clientDir, "tufrepo", "metadata", "current") + prevDir := filepath.Join(clientDir, "tufrepo", "metadata", "previous") + c.Assert(os.MkdirAll(currDir, 0755), IsNil) + c.Assert(os.MkdirAll(prevDir, 0755), IsNil) + rootJSON, err := os.ReadFile(filepath.Join(dir, "repository", "1.root.json")) + c.Assert(err, IsNil) + c.Assert(os.WriteFile(filepath.Join(currDir, "root.json"), rootJSON, 0644), IsNil) + + args := []string{ + filepath.Join(cwd, "testdata", "python-tuf-v2.0.0", "client.py"), "--repo=http://" + addr + "/" + name, } for path := range files { diff --git a/client/python_interop/testdata/Makefile b/client/python_interop/testdata/Makefile index f5ef1630..3b84759a 100644 --- a/client/python_interop/testdata/Makefile +++ b/client/python_interop/testdata/Makefile @@ -1,4 +1,4 @@ -PYTHON_TUF=python-tuf-v1.0.0 +PYTHON_TUF=python-tuf-v2.0.0 all: docker build -t tuf-gen ./$(PYTHON_TUF) diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.root.json b/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.root.json deleted file mode 100644 index 13ccb178..00000000 --- a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.root.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "signatures": [ - { - "keyid": "e26489b1801b03e9e770b18f34c337cc29c1b02c4a4385ac698dfe879753e6b3", - "sig": "6385f433122fbe8760030371c0e6768cecff0df2fb22fdf04618da9f09b230070e3ef23f4d13978993466f2f1f983e4d712443c23bde08a5be77df6e6832be0e" - } - ], - "signed": { - "_type": "root", - "consistent_snapshot": true, - "expires": "2030-01-01T00:00:00Z", - "keys": { - "7b1aa7dbc1b24fb44d0146b0fe726d92f9640e26dc197d50345f9b00e0c98c4a": { - "keytype": "ed25519", - "keyval": { - "public": "98b7918168c7adfe0cdc26bd332baf67335ac4e8be350ddc3d27e54b93ae8b41" - }, - "scheme": "ed25519" - }, - "7fe79f52b6ab730028c299b82e4c45c828b3a47fea06f9e4582597a62c4e2f9a": { - "keytype": "ed25519", - "keyval": { - "public": "baa12f5d0f6d053c27625240b021286c9d8939ae62ec6143ee9ef1ada31fc0d9" - }, - "scheme": "ed25519" - }, - "d72e12473433414326514f50289712cb2a4fd8b518b70fcea077018d50a702f2": { - "keytype": "ed25519", - "keyval": { - "public": "ab5ae139b430f0210e4b3ae024882e9bf8c7d31e6ae958061da8ab51ce162c3c" - }, - "scheme": "ed25519" - }, - "e26489b1801b03e9e770b18f34c337cc29c1b02c4a4385ac698dfe879753e6b3": { - "keytype": "ed25519", - "keyval": { - "public": "aef72f53b1aeed769cc5678dbdc8a104d562c87e8201278156f84bab2c0c0a30" - }, - "scheme": "ed25519" - } - }, - "roles": { - "root": { - "keyids": [ - "e26489b1801b03e9e770b18f34c337cc29c1b02c4a4385ac698dfe879753e6b3" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "d72e12473433414326514f50289712cb2a4fd8b518b70fcea077018d50a702f2" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "7b1aa7dbc1b24fb44d0146b0fe726d92f9640e26dc197d50345f9b00e0c98c4a" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "7fe79f52b6ab730028c299b82e4c45c828b3a47fea06f9e4582597a62c4e2f9a" - ], - "threshold": 1 - } - }, - "spec_version": "1.0.19", - "version": 1 - } -} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.snapshot.json b/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.snapshot.json deleted file mode 100644 index 4a792fe1..00000000 --- a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.snapshot.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "signatures": [ - { - "keyid": "d72e12473433414326514f50289712cb2a4fd8b518b70fcea077018d50a702f2", - "sig": "da8a6d4ec27acf62c23d16b51430456d12a20b3373ae52ecf3461ae52f7643dafcc51ca76f5080e9441c055e6a888323fb41e40427df4fc16fea5f35b358db04" - } - ], - "signed": { - "_type": "snapshot", - "expires": "2030-01-01T00:00:00Z", - "meta": { - "targets.json": { - "version": 1 - } - }, - "spec_version": "1.0.19", - "version": 1 - } -} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/timestamp.json b/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/timestamp.json deleted file mode 100644 index c86ca151..00000000 --- a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/timestamp.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signatures": [ - { - "keyid": "7fe79f52b6ab730028c299b82e4c45c828b3a47fea06f9e4582597a62c4e2f9a", - "sig": "a10f63e6ce554a259ed8e49afbfc6dc358fad05a377ed9f237ba0d81a058fb995be6ab2a4d67166532203fc68239bd8eaf1ebbbe9730c88773b27479d7395a0a" - } - ], - "signed": { - "_type": "timestamp", - "expires": "2030-01-01T00:00:00Z", - "meta": { - "snapshot.json": { - "hashes": { - "sha256": "e92738a47826131048e88e6850dab1667c2e4934514c294a478670e5ec230514" - }, - "length": 432, - "version": 1 - } - }, - "spec_version": "1.0.19", - "version": 1 - } -} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/1.root.json b/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/1.root.json deleted file mode 100644 index a278bf24..00000000 --- a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/1.root.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "signatures": [ - { - "keyid": "cbc434dd8f82c00935912ae123555c8f1d8e202ee74ed3511474a511fbd2bc3b", - "sig": "fb9652e344f028d864391f3bd897cc9e2e8c7a2bd7498cdf11816e517f7eedcf5b79a39757d4891e20d8c4917f6b1ef2d6fe729feb54bda771432f8664a48500" - } - ], - "signed": { - "_type": "root", - "consistent_snapshot": false, - "expires": "2030-01-01T00:00:00Z", - "keys": { - "6694e8c2a2d3cf98ebe41d2442235c46c7caaa4bd8908036d45ac5b046b08dde": { - "keytype": "ed25519", - "keyval": { - "public": "ce1de9af6e9f0599ade37d947e4fd19bc4d89da7ab3f335055196b42f6fa1704" - }, - "scheme": "ed25519" - }, - "ae6fcd48721c779d066643ed9d895feb48a86039732e93b58895d99bbbd0c0ae": { - "keytype": "ed25519", - "keyval": { - "public": "bb7e06355dee36ba33aa70780f82477724a78d69b9d7de3ebd647ff0d648516f" - }, - "scheme": "ed25519" - }, - "cbc434dd8f82c00935912ae123555c8f1d8e202ee74ed3511474a511fbd2bc3b": { - "keytype": "ed25519", - "keyval": { - "public": "e88e416ada03c8ef6b825aa43ff2c608683c51efa752234a454f9b1f9c5a2a39" - }, - "scheme": "ed25519" - }, - "e03d20021373f9de91fccedeb6572438b6803f2d3ab37adf51b72dc4aa527655": { - "keytype": "ed25519", - "keyval": { - "public": "95c4cabf0783bf904943179f5db97b8210d745b0dd4bfa4659f0c172b80d6e08" - }, - "scheme": "ed25519" - } - }, - "roles": { - "root": { - "keyids": [ - "cbc434dd8f82c00935912ae123555c8f1d8e202ee74ed3511474a511fbd2bc3b" - ], - "threshold": 1 - }, - "snapshot": { - "keyids": [ - "6694e8c2a2d3cf98ebe41d2442235c46c7caaa4bd8908036d45ac5b046b08dde" - ], - "threshold": 1 - }, - "targets": { - "keyids": [ - "ae6fcd48721c779d066643ed9d895feb48a86039732e93b58895d99bbbd0c0ae" - ], - "threshold": 1 - }, - "timestamp": { - "keyids": [ - "e03d20021373f9de91fccedeb6572438b6803f2d3ab37adf51b72dc4aa527655" - ], - "threshold": 1 - } - }, - "spec_version": "1.0.19", - "version": 1 - } -} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/snapshot.json b/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/snapshot.json deleted file mode 100644 index 30668fb5..00000000 --- a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/snapshot.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "signatures": [ - { - "keyid": "6694e8c2a2d3cf98ebe41d2442235c46c7caaa4bd8908036d45ac5b046b08dde", - "sig": "6c06bd2dc1f9b9991ff68fa3edb9b3b6becee25f638b4187e161ac65cadcbb59d320196bb0fb72cf08734b75531f3ceddefdc56424ec8a8c8fee068b2584450d" - } - ], - "signed": { - "_type": "snapshot", - "expires": "2030-01-01T00:00:00Z", - "meta": { - "targets.json": { - "version": 1 - } - }, - "spec_version": "1.0.19", - "version": 1 - } -} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/timestamp.json b/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/timestamp.json deleted file mode 100644 index 5a9d3c31..00000000 --- a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/timestamp.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "signatures": [ - { - "keyid": "e03d20021373f9de91fccedeb6572438b6803f2d3ab37adf51b72dc4aa527655", - "sig": "50315d16c729d2068586776285f6282c87cd722e052acf293166ca1cf7313bdc1a673d02309f9be2d710d82c239ce349f3bffbcb76fbebb655bea2199db92503" - } - ], - "signed": { - "_type": "timestamp", - "expires": "2030-01-01T00:00:00Z", - "meta": { - "snapshot.json": { - "hashes": { - "sha256": "4471c47119f7b16307f9955e7fa0fdcaf53c82f7a857ea2b2f46fd6fe9bc53e9" - }, - "length": 432, - "version": 1 - } - }, - "spec_version": "1.0.19", - "version": 1 - } -} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/Dockerfile b/client/python_interop/testdata/python-tuf-v2.0.0/Dockerfile similarity index 57% rename from client/python_interop/testdata/python-tuf-v1.0.0/Dockerfile rename to client/python_interop/testdata/python-tuf-v2.0.0/Dockerfile index 3987fe06..10b4f445 100644 --- a/client/python_interop/testdata/python-tuf-v1.0.0/Dockerfile +++ b/client/python_interop/testdata/python-tuf-v2.0.0/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.10 RUN apt-get update && apt-get install -y libsodium-dev tree -RUN pip install 'securesystemslib[crypto,pynacl]==0.22.0' 'tuf==v1.0.0' +RUN pip install -U pip && pip install 'securesystemslib[crypto,pynacl]==0.25.0' 'tuf==v2.0.0' ADD generate.py generate.sh / CMD /generate.sh diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/client.py b/client/python_interop/testdata/python-tuf-v2.0.0/client.py similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/client.py rename to client/python_interop/testdata/python-tuf-v2.0.0/client.py diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/generate.py b/client/python_interop/testdata/python-tuf-v2.0.0/generate.py similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/generate.py rename to client/python_interop/testdata/python-tuf-v2.0.0/generate.py diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/generate.sh b/client/python_interop/testdata/python-tuf-v2.0.0/generate.sh similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/generate.sh rename to client/python_interop/testdata/python-tuf-v2.0.0/generate.sh diff --git a/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.root.json b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.root.json new file mode 100755 index 00000000..1d7d8394 --- /dev/null +++ b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "7e576308aa26b10a124e13fd6bc4d63977d1904db6823c2862636d2b330fd669", + "sig": "c50fb9e20819b06d8dbebc748a269f1b9e089a2d8a81f64f7e889c054e619ec22c98e4ae24cb868b7c7757f62853cb0cd5dd658dad5ba94286a34255f5f7a100" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "7e576308aa26b10a124e13fd6bc4d63977d1904db6823c2862636d2b330fd669": { + "keytype": "ed25519", + "keyval": { + "public": "018908d2190610e54be1f6af0c77a7f59efc5b42971575a06e66018ae36bf15c" + }, + "scheme": "ed25519" + }, + "e76b314161b74584dbda0b3e46eaf1b9feea6f85abc0d9c8432e7fe3752d4f0f": { + "keytype": "ed25519", + "keyval": { + "public": "ecfe22e7fb9c2d867bad321ce90f297d5747efce8d96af171b5d5801b13a1c72" + }, + "scheme": "ed25519" + }, + "fa5b604a0ee5980e5f8a2398729e03c539fa68123cd3eb2e99f70d3504a71704": { + "keytype": "ed25519", + "keyval": { + "public": "128d5aba7c967e6e87c4e06b852282c91dd90c9ac9dec3adc9e6a237d4530752" + }, + "scheme": "ed25519" + }, + "faf425a6c41e594be56b35ab08c0dfe486ab6e14a25308bbfa990e050cdf0aef": { + "keytype": "ed25519", + "keyval": { + "public": "2a70c65b98f325026225b568af3587e993a0a5ab96671469e4ecfbb386412063" + }, + "scheme": "ed25519" + } + }, + "roles": { + "root": { + "keyids": [ + "7e576308aa26b10a124e13fd6bc4d63977d1904db6823c2862636d2b330fd669" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "fa5b604a0ee5980e5f8a2398729e03c539fa68123cd3eb2e99f70d3504a71704" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "faf425a6c41e594be56b35ab08c0dfe486ab6e14a25308bbfa990e050cdf0aef" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "e76b314161b74584dbda0b3e46eaf1b9feea6f85abc0d9c8432e7fe3752d4f0f" + ], + "threshold": 1 + } + }, + "spec_version": "1.0.19", + "version": 1 + } +} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.snapshot.json b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.snapshot.json new file mode 100755 index 00000000..fbf2d1cb --- /dev/null +++ b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.snapshot.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "fa5b604a0ee5980e5f8a2398729e03c539fa68123cd3eb2e99f70d3504a71704", + "sig": "1c09c1ca4c38e68f4ba9ac48c6ed6fcee13e5a071dcfeb51e7b5cf1a823bcc16b26528f67efaa59383a4a765cdd5fd81d67f74a5c09789673cab164a7251b20d" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1 + } + }, + "spec_version": "1.0.19", + "version": 1 + } +} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.targets.json b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.targets.json old mode 100644 new mode 100755 similarity index 67% rename from client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.targets.json rename to client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.targets.json index 402978bb..5acb7c64 --- a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/metadata/1.targets.json +++ b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/1.targets.json @@ -1,8 +1,8 @@ { "signatures": [ { - "keyid": "7b1aa7dbc1b24fb44d0146b0fe726d92f9640e26dc197d50345f9b00e0c98c4a", - "sig": "012a5a56af043b44fb913d859b61f85800c72fd93b5b3a04ac20f988c29114c643e933f427ff4d38b29b5b2340fe76cfa05704e075d799431fbafe80f9733500" + "keyid": "faf425a6c41e594be56b35ab08c0dfe486ab6e14a25308bbfa990e050cdf0aef", + "sig": "42e49b11e043fa847b0f82786d5fb44988ab0281bda02553e54eb95d7fdbeea25b95c81b824c900cf6132d4714d3ad4d88786155d915603859d27c7934d5650f" } ], "signed": { diff --git a/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/timestamp.json b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/timestamp.json new file mode 100755 index 00000000..23ab4e54 --- /dev/null +++ b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/metadata/timestamp.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "e76b314161b74584dbda0b3e46eaf1b9feea6f85abc0d9c8432e7fe3752d4f0f", + "sig": "9c734040db3786f9b8f44db282f53f054fca99654ea393601430adb0d1204c44ab534568b4412b52a8578f35306720903117758abb4534dc8c15ad5a9ebe7e09" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1 + } + }, + "spec_version": "1.0.19", + "version": 1 + } +} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/targets/55ae75d991c770d8f3ef07cbfde124ffce9c420da5db6203afab700b27e10cf9.file1.txt b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/targets/55ae75d991c770d8f3ef07cbfde124ffce9c420da5db6203afab700b27e10cf9.file1.txt similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/targets/55ae75d991c770d8f3ef07cbfde124ffce9c420da5db6203afab700b27e10cf9.file1.txt rename to client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/targets/55ae75d991c770d8f3ef07cbfde124ffce9c420da5db6203afab700b27e10cf9.file1.txt diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/targets/dir/04e2f59431a9d219321baf7d21b8cc797d7615dc3e9515c782c49d2075658701.file2.txt b/client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/targets/dir/04e2f59431a9d219321baf7d21b8cc797d7615dc3e9515c782c49d2075658701.file2.txt similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/with-consistent-snapshot/repository/targets/dir/04e2f59431a9d219321baf7d21b8cc797d7615dc3e9515c782c49d2075658701.file2.txt rename to client/python_interop/testdata/python-tuf-v2.0.0/with-consistent-snapshot/repository/targets/dir/04e2f59431a9d219321baf7d21b8cc797d7615dc3e9515c782c49d2075658701.file2.txt diff --git a/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/1.root.json b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/1.root.json new file mode 100755 index 00000000..cf2d4494 --- /dev/null +++ b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/1.root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "3838ee205d1906276ddb1df0803192f95753104e4dd828b70f17e3b3aa90de75", + "sig": "fb2b85aea59950fb7ca3012fd93d252ba7c1cec30c2afae0f1b4fb0146158e0800ad0e99221123b396e0a1a487b5cafdfe50d94a893e7dd5ede2891341a31f00" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": false, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "3838ee205d1906276ddb1df0803192f95753104e4dd828b70f17e3b3aa90de75": { + "keytype": "ed25519", + "keyval": { + "public": "4203975790ecf4aba558cb7ab3f9430c51b4c3553117d09949f3123fb49d4961" + }, + "scheme": "ed25519" + }, + "4908b7ba44138d79f05f3d211cb5f4bf9e73a71a7bc4ecd1b263accff7632445": { + "keytype": "ed25519", + "keyval": { + "public": "9dc9bf2210f5b743e3b094eae198c1bb2633966d6c10ac2a6d54b782623b8b89" + }, + "scheme": "ed25519" + }, + "8c0291ae0d3c260405e6683cd2260ee6fbb5676deb57541acbe5b94ca4089a50": { + "keytype": "ed25519", + "keyval": { + "public": "ca9a2dfe087762bea03a556ddc01c9ed113f5651ca1546804dae39441099fca3" + }, + "scheme": "ed25519" + }, + "c0ef91ecbb8a59bffc5642ba01129d64059b076b946d120733ea949e2c7786a2": { + "keytype": "ed25519", + "keyval": { + "public": "fcd7c34b20c3b8093b6b21b6a505f715e3d0645c1d9e05ee14d29f045e0b41d8" + }, + "scheme": "ed25519" + } + }, + "roles": { + "root": { + "keyids": [ + "3838ee205d1906276ddb1df0803192f95753104e4dd828b70f17e3b3aa90de75" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "c0ef91ecbb8a59bffc5642ba01129d64059b076b946d120733ea949e2c7786a2" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "4908b7ba44138d79f05f3d211cb5f4bf9e73a71a7bc4ecd1b263accff7632445" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "8c0291ae0d3c260405e6683cd2260ee6fbb5676deb57541acbe5b94ca4089a50" + ], + "threshold": 1 + } + }, + "spec_version": "1.0.19", + "version": 1 + } +} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/snapshot.json b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/snapshot.json new file mode 100755 index 00000000..333bc5dd --- /dev/null +++ b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/snapshot.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "c0ef91ecbb8a59bffc5642ba01129d64059b076b946d120733ea949e2c7786a2", + "sig": "44b9651d75522ac5bdb13e23706f37ade578c59355525ac473d14de4f3cb5c5ace9c2d32b552754305f665e4e91c6773c6876e704939200bb71ab0e03a59ce03" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "targets.json": { + "version": 1 + } + }, + "spec_version": "1.0.19", + "version": 1 + } +} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/targets.json b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/targets.json old mode 100644 new mode 100755 similarity index 67% rename from client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/targets.json rename to client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/targets.json index ed805ca8..e224e70b --- a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/metadata/targets.json +++ b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/targets.json @@ -1,8 +1,8 @@ { "signatures": [ { - "keyid": "ae6fcd48721c779d066643ed9d895feb48a86039732e93b58895d99bbbd0c0ae", - "sig": "50eec0672e2861e3b73a8a4a15cc60113dc5b478d1431a55d90f8ab65a1954acbbc860f586f3a331144e41a56c54e54c49d9d9eba6ace2c553ec542aafcf4c04" + "keyid": "4908b7ba44138d79f05f3d211cb5f4bf9e73a71a7bc4ecd1b263accff7632445", + "sig": "faf60abd2a725d5962c8a708d9d298d25accbe379b01dc9a5516161b702287b57d425d927942c1a5ddf462be1f8c12718575de5c29994bbff7d83c6218fbc00b" } ], "signed": { diff --git a/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/timestamp.json b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/timestamp.json new file mode 100755 index 00000000..915f07af --- /dev/null +++ b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/metadata/timestamp.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "8c0291ae0d3c260405e6683cd2260ee6fbb5676deb57541acbe5b94ca4089a50", + "sig": "933aba2b1f567c8fc6c8b08850330449be6c83a30a75f4d978600b5470f69416bbd0f6cb6cf2c088a0bc2d948290123a2e6419908c58b1b32772aa8f2f94490e" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1 + } + }, + "spec_version": "1.0.19", + "version": 1 + } +} \ No newline at end of file diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/targets/dir/file2.txt b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/targets/dir/file2.txt similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/targets/dir/file2.txt rename to client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/targets/dir/file2.txt diff --git a/client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/targets/file1.txt b/client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/targets/file1.txt similarity index 100% rename from client/python_interop/testdata/python-tuf-v1.0.0/without-consistent-snapshot/repository/targets/file1.txt rename to client/python_interop/testdata/python-tuf-v2.0.0/without-consistent-snapshot/repository/targets/file1.txt diff --git a/cmd/tuf-client/main.go b/cmd/tuf-client/main.go index 15d03fad..df3632b2 100644 --- a/cmd/tuf-client/main.go +++ b/cmd/tuf-client/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "os" docopt "github.com/flynn/go-docopt" tuf "github.com/theupdateframework/go-tuf/client" @@ -32,7 +33,7 @@ See "tuf-client help " for more information on a specific command. if cmd == "help" { if len(cmdArgs) == 0 { // `tuf-client help` - fmt.Println(usage) + fmt.Fprint(os.Stdout, usage) return } else { // `tuf-client help ` cmd = cmdArgs[0] diff --git a/cmd/tuf/gen_key.go b/cmd/tuf/gen_key.go index bd4334ae..2ad77a58 100644 --- a/cmd/tuf/gen_key.go +++ b/cmd/tuf/gen_key.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "time" "github.com/flynn/go-docopt" @@ -39,7 +40,7 @@ func cmdGenKey(args *docopt.Args, repo *tuf.Repo) error { string(data.KeySchemeRSASSA_PSS_SHA256): keyScheme = data.KeyScheme(t) default: - fmt.Println("Using default key scheme", keyScheme) + fmt.Fprint(os.Stderr, "Using default key scheme", keyScheme) } var err error @@ -57,7 +58,7 @@ func cmdGenKey(args *docopt.Args, repo *tuf.Repo) error { return err } for _, id := range keyids { - fmt.Println("Generated", role, keyScheme, "key with ID", id) + fmt.Fprintf(os.Stdout, "Generated %s %s key with ID %s", role, keyScheme, id) } return nil } diff --git a/cmd/tuf/get_threshold.go b/cmd/tuf/get_threshold.go index e40ec26e..a0d78fdd 100644 --- a/cmd/tuf/get_threshold.go +++ b/cmd/tuf/get_threshold.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "github.com/flynn/go-docopt" "github.com/theupdateframework/go-tuf" @@ -23,6 +24,6 @@ func cmdGetThreshold(args *docopt.Args, repo *tuf.Repo) error { return err } - fmt.Println("The threshold for", role, "role is", threshold) + fmt.Fprintf(os.Stdout, "The threshold for %s role is %d", role, threshold) return nil } diff --git a/cmd/tuf/main.go b/cmd/tuf/main.go index f2b73972..dc6a256c 100644 --- a/cmd/tuf/main.go +++ b/cmd/tuf/main.go @@ -58,7 +58,7 @@ See "tuf help " for more information on a specific command if cmd == "help" { if len(cmdArgs) == 0 { // `tuf help` - fmt.Println(usage) + fmt.Fprint(os.Stdout, usage) return } else { // `tuf help ` cmd = cmdArgs[0] @@ -115,7 +115,11 @@ func runCommand(name string, args []string, dir string, insecure bool) error { if !insecure { p = getPassphrase } - repo, err := tuf.NewRepo(tuf.FileSystemStore(dir, p)) + logger := log.New(os.Stdout, "", 0) + storeOpts := tuf.StoreOpts{Logger: logger, PassFunc: p} + + repo, err := tuf.NewRepoWithOpts(tuf.FileSystemStoreWithOpts(dir, storeOpts), + tuf.WithLogger(logger)) if err != nil { return err } diff --git a/cmd/tuf/payload.go b/cmd/tuf/payload.go index 8cc0c2ff..3ae2c891 100644 --- a/cmd/tuf/payload.go +++ b/cmd/tuf/payload.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "github.com/flynn/go-docopt" "github.com/theupdateframework/go-tuf" @@ -20,6 +21,6 @@ func cmdPayload(args *docopt.Args, repo *tuf.Repo) error { if err != nil { return err } - fmt.Print(string(p)) + fmt.Fprint(os.Stdout, string(p)) return nil } diff --git a/cmd/tuf/set_threshold.go b/cmd/tuf/set_threshold.go index 57754d24..29149ff9 100644 --- a/cmd/tuf/set_threshold.go +++ b/cmd/tuf/set_threshold.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "strconv" "github.com/flynn/go-docopt" @@ -28,6 +29,6 @@ func cmdSetThreshold(args *docopt.Args, repo *tuf.Repo) error { return err } - fmt.Println("The threshold for", role, "role is now", threshold) + fmt.Fprintf(os.Stdout, "The threshold for %s role is now %d", role, threshold) return nil } diff --git a/cmd/tuf/sign_payload.go b/cmd/tuf/sign_payload.go index 8da5642b..6772972c 100644 --- a/cmd/tuf/sign_payload.go +++ b/cmd/tuf/sign_payload.go @@ -36,7 +36,7 @@ func cmdSignPayload(args *docopt.Args, repo *tuf.Repo) error { if err != nil { return err } - fmt.Print(string(bytes)) + fmt.Fprint(os.Stdout, string(bytes)) fmt.Fprintln(os.Stderr, "tuf: signed with", numKeys, "key(s)") return nil diff --git a/data/types.go b/data/types.go index d051b762..3e1806bd 100644 --- a/data/types.go +++ b/data/types.go @@ -172,11 +172,14 @@ func (f Hashes) HashAlgorithms() []string { } type metapathFileMeta struct { - Length int64 `json:"length,omitempty"` - Hashes Hashes `json:"hashes,omitempty"` - Version int64 `json:"version"` + Length int64 `json:"length,omitempty"` + Hashes Hashes `json:"hashes,omitempty"` + Version int64 `json:"version"` + Custom *json.RawMessage `json:"custom,omitempty"` } +// SnapshotFileMeta is the meta field of a snapshot +// Note: Contains a `custom` field type SnapshotFileMeta metapathFileMeta type SnapshotFiles map[string]SnapshotFileMeta diff --git a/docs/MAINTAINERS b/docs/MAINTAINERS index d6b39ded..70935e60 100644 --- a/docs/MAINTAINERS +++ b/docs/MAINTAINERS @@ -1,8 +1,6 @@ Asra Ali (github: asraa) Trishank Karthik Kuppusamy (github: trishankatdatadog) Joshua Lock (github: joshuagl) -Ethan Lowman (github: ethan-lowman-dd) Marina Moore (github: mnm678) Zack Newman (github: znewman01) -Hossein Siadati (github: hosseinsia) Radoslav Dimitrov (github: rdimitrov) diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 00000000..eb26fed7 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +The go-tuf community is committed to maintaining a reliable and consistent TUF client implementation. If you believe you have identified a security issue in go-tuf's client protocol, please follow these guidelines for responsible disclosure. + +## Supported Versions + +You may report issues for the most recent version of go-tuf. We will not retroactively make changes to older versions. + +## Reporting a Vulnerability + +If you discover a potential security issue in this project we ask that you notify the go-tuf maintainers via [Github's private reporting feature](https://github.com/theupdateframework/go-tuf/security/advisories/new) (requires being signed in to GitHub). At the minimum, the report must contain the following: + +* A description of the issue. +* A specific version or commit SHA of `go-tuf` where the issue reproduces. +* Instructions to reproduce the issue. + +Please do **not** create a public GitHub issue or pull request to submit vulnerability reports. These public trackers are intended for non-time-sensitive and non-security-related bug reports and feature requests. Major feature requests, such as design changes to the specification, should be proposed via a [TUF Augmentation Protocol](https://theupdateframework.github.io/specification/latest/#tuf-augmentation-proposal-tap-support) (TAP). + +## Disclosure + +This project follows a 90 day disclosure timeline. diff --git a/docs/TESTING.md b/docs/TESTING.md index 3a00a95b..5549853c 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,6 +1,6 @@ # Testing -The [Python interoperability tests](client/python_interop/) require Python 3 +The [Python interoperability tests](../client/python_interop/) require Python 3 (available as `python` on the `$PATH`) and the [`python-tuf` package](https://github.com/theupdateframework/python-tuf) installed. To use the correct versions of the packages, it is recommended to use a [virtual environment](https://docs.python.org/3/library/venv.html#module-venv) and install the dependencies via: @@ -15,4 +15,4 @@ go test ./... To update the data for these tests requires Docker and make (see -test data [README.md](client/python_interop/testdata/README.md) for details). \ No newline at end of file +test data [README.md](../client/python_interop/testdata/README.md) for details). diff --git a/go.mod b/go.mod index a5151da0..f1b26376 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/theupdateframework/go-tuf go 1.18 require ( - github.com/dustin/go-humanize v1.0.0 + github.com/dustin/go-humanize v1.0.1 github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e github.com/google/gofuzz v1.2.0 github.com/secure-systems-lab/go-securesystemslib v0.4.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 @@ -19,9 +19,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.1.0 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ff41cd25..067d7581 100644 --- a/go.sum +++ b/go.sum @@ -4,13 +4,13 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e h1:Ss/B3/5wWRh8+emnK0++g5zQzwDTi30W10pKxKc4JXI= github.com/flynn/go-docopt v0.0.0-20140912013429-f6dd2ebbb31e/go.mod h1:HyVoz1Mz5Co8TFO8EupIdlcpwShBmY98dkT2xeHkvEI= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -43,15 +43,14 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -59,13 +58,13 @@ github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/ github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -79,12 +78,10 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -97,23 +94,20 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= @@ -124,7 +118,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -143,8 +136,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/fsutil/perm_test.go b/internal/fsutil/perm_test.go index 6061d291..f80ef94a 100644 --- a/internal/fsutil/perm_test.go +++ b/internal/fsutil/perm_test.go @@ -4,7 +4,6 @@ package fsutil import ( - "fmt" "os" "path/filepath" "testing" @@ -59,7 +58,6 @@ func TestEnsureMaxPermissions(t *testing.T) { assert.NoError(t, err) err = EnsureMaxPermissions(fi, os.FileMode(0222)) assert.Error(t, err) - fmt.Println(err) // Check matching due to more restrictive perms on file err = os.Chmod(p, 0444) diff --git a/local_store.go b/local_store.go index 1b4a7f60..fee03f31 100644 --- a/local_store.go +++ b/local_store.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/fs" + "log" "os" "path/filepath" "strings" @@ -197,18 +198,44 @@ type persistedKeys struct { Data json.RawMessage `json:"data"` } +type StoreOpts struct { + Logger *log.Logger + PassFunc util.PassphraseFunc +} + func FileSystemStore(dir string, p util.PassphraseFunc) LocalStore { return &fileSystemStore{ dir: dir, passphraseFunc: p, + logger: log.New(io.Discard, "", 0), signerForKeyID: make(map[string]keys.Signer), keyIDsForRole: make(map[string][]string), } } +func FileSystemStoreWithOpts(dir string, opts ...StoreOpts) LocalStore { + store := &fileSystemStore{ + dir: dir, + passphraseFunc: nil, + logger: log.New(io.Discard, "", 0), + signerForKeyID: make(map[string]keys.Signer), + keyIDsForRole: make(map[string][]string), + } + for _, opt := range opts { + if opt.Logger != nil { + store.logger = opt.Logger + } + if opt.PassFunc != nil { + store.passphraseFunc = opt.PassFunc + } + } + return store +} + type fileSystemStore struct { dir string passphraseFunc util.PassphraseFunc + logger *log.Logger signerForKeyID map[string]keys.Signer keyIDsForRole map[string][]string @@ -526,7 +553,7 @@ func (f *fileSystemStore) ChangePassphrase(role string) error { keys, _, err := f.loadPrivateKeys(role) if err != nil { if os.IsNotExist(err) { - fmt.Printf("Failed to change passphrase. Missing keys file for %s role. \n", role) + f.logger.Printf("Failed to change passphrase. Missing keys file for %s role. \n", role) } return err } @@ -548,7 +575,7 @@ func (f *fileSystemStore) ChangePassphrase(role string) error { if err := util.AtomicallyWriteFile(f.keysPath(role), append(data, '\n'), 0600); err != nil { return err } - fmt.Printf("Successfully changed passphrase for %s keys file\n", role) + f.logger.Printf("Successfully changed passphrase for %s keys file\n", role) return nil } diff --git a/pkg/keys/deprecated_ecdsa.go b/pkg/keys/deprecated_ecdsa.go index 4a8f151e..6d48c9d6 100644 --- a/pkg/keys/deprecated_ecdsa.go +++ b/pkg/keys/deprecated_ecdsa.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "os" "github.com/theupdateframework/go-tuf/data" ) @@ -98,6 +97,5 @@ func (p *deprecatedP256Verifier) UnmarshalPublicKey(key *data.PublicKey) error { } p.key = key - fmt.Fprintln(os.Stderr, "tuf: warning using deprecated ecdsa hex-encoded keys") return nil } diff --git a/pkg/targets/delegation.go b/pkg/targets/delegation.go index ccd52bae..dce61710 100644 --- a/pkg/targets/delegation.go +++ b/pkg/targets/delegation.go @@ -18,6 +18,7 @@ type delegationsIterator struct { stack []Delegation target string visitedRoles map[string]struct{} + parents map[string]string } var ErrTopLevelTargetsRoleMissing = errors.New("tuf: top level targets role missing from top level keys DB") @@ -43,6 +44,7 @@ func NewDelegationsIterator(target string, topLevelKeysDB *verify.DB) (*delegati }, }, visitedRoles: make(map[string]struct{}), + parents: make(map[string]string), } return i, nil } @@ -88,8 +90,13 @@ func (d *delegationsIterator) Add(roles []data.DelegatedRole, delegator string, DB: db, } d.stack = append(d.stack, delegation) + d.parents[r.Name] = delegator } } return nil } + +func (d *delegationsIterator) Parent(role string) string { + return d.parents[role] +} diff --git a/repo.go b/repo.go index 603785f1..c6a23dee 100644 --- a/repo.go +++ b/repo.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "path" "sort" "strings" @@ -48,18 +49,47 @@ type Repo struct { meta map[string]json.RawMessage prefix string indent string + logger *log.Logger +} + +type RepoOpts func(r *Repo) + +func WithLogger(logger *log.Logger) RepoOpts { + return func(r *Repo) { + r.logger = logger + } +} + +func WithHashAlgorithms(hashAlgorithms ...string) RepoOpts { + return func(r *Repo) { + r.hashAlgorithms = hashAlgorithms + } +} + +func WithPrefix(prefix string) RepoOpts { + return func(r *Repo) { + r.prefix = prefix + } +} + +func WithIndex(indent string) RepoOpts { + return func(r *Repo) { + r.indent = indent + } } func NewRepo(local LocalStore, hashAlgorithms ...string) (*Repo, error) { return NewRepoIndent(local, "", "", hashAlgorithms...) } -func NewRepoIndent(local LocalStore, prefix string, indent string, hashAlgorithms ...string) (*Repo, error) { +func NewRepoIndent(local LocalStore, prefix string, indent string, + hashAlgorithms ...string) (*Repo, error) { r := &Repo{ local: local, hashAlgorithms: hashAlgorithms, prefix: prefix, indent: indent, + logger: log.New(io.Discard, "", 0), } var err error @@ -70,6 +100,17 @@ func NewRepoIndent(local LocalStore, prefix string, indent string, hashAlgorithm return r, nil } +func NewRepoWithOpts(local LocalStore, opts ...RepoOpts) (*Repo, error) { + r, err := NewRepo(local) + if err != nil { + return nil, err + } + for _, opt := range opts { + opt(r) + } + return r, nil +} + func (r *Repo) Init(consistentSnapshot bool) error { t, err := r.topLevelTargets() if err != nil { @@ -91,7 +132,7 @@ func (r *Repo) Init(consistentSnapshot bool) error { return err } - fmt.Println("Repository initialized") + r.logger.Println("Repository initialized") return nil } @@ -533,7 +574,7 @@ func (r *Repo) RevokeKeyWithExpires(keyRole, id string, expires time.Time) error err = r.setMeta("root.json", root) if err == nil { - fmt.Println("Revoked", keyRole, "key with ID", id, "in root metadata") + r.logger.Println("Revoked", keyRole, "key with ID", id, "in root metadata") } return err } @@ -657,8 +698,7 @@ func (r *Repo) ResetTargetsDelegationsWithExpires(delegator string, expires time return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err) } - t.Delegations = &data.Delegations{} - t.Delegations.Keys = make(map[string]*data.PublicKey) + t.Delegations = nil t.Expires = expires.Round(time.Second) @@ -783,7 +823,7 @@ func (r *Repo) Sign(roleFilename string) error { r.meta[roleFilename] = b err = r.local.SetMeta(roleFilename, b) if err == nil { - fmt.Println("Signed", roleFilename, "with", numKeys, "key(s)") + r.logger.Println("Signed", roleFilename, "with", numKeys, "key(s)") } return err } @@ -803,7 +843,13 @@ func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signatur return ErrInvalidRole{role, "no trusted keys for role"} } + s, err := r.SignedMeta(roleFilename) + if err != nil { + return err + } + keyInDB := false + validSig := false for _, db := range dbs { roleData := db.GetRole(role) if roleData == nil { @@ -811,15 +857,27 @@ func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signatur } if roleData.ValidKey(signature.KeyID) { keyInDB = true + + verifier, err := db.GetVerifier(signature.KeyID) + if err != nil { + continue + } + + // Now check if this validly signed the metadata. + if err := verify.VerifySignature(s.Signed, signature.Signature, + verifier); err == nil { + validSig = true + break + } } } if !keyInDB { + // This key was not delegated for the role in any delegatee. return verify.ErrInvalidKey } - - s, err := r.SignedMeta(roleFilename) - if err != nil { - return err + if !validSig { + // The signature was invalid. + return verify.ErrInvalid } // Add or update signature. @@ -832,16 +890,6 @@ func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signatur signatures = append(signatures, signature) s.Signatures = signatures - // Check signature on signed meta. Ignore threshold errors as this may not be fully - // signed. - for _, db := range dbs { - if err := db.VerifySignatures(s, role); err != nil { - if _, ok := err.(verify.ErrRoleThreshold); !ok { - return err - } - } - } - b, err := r.jsonMarshal(s) if err != nil { return err @@ -1223,7 +1271,7 @@ func (r *Repo) removeTargetsWithExpiresFromMeta(metaName string, paths []string, for _, path := range paths { path = util.NormalizeTarget(path) if _, ok := t.Targets[path]; !ok { - fmt.Printf("[%v] The following target is not present: %v\n", metaName, path) + r.logger.Printf("[%v] The following target is not present: %v\n", metaName, path) continue } removed = true @@ -1243,17 +1291,17 @@ func (r *Repo) removeTargetsWithExpiresFromMeta(metaName string, paths []string, err = r.setMeta(metaName, t) if err == nil { - fmt.Printf("[%v] Removed targets:\n", metaName) + r.logger.Printf("[%v] Removed targets:\n", metaName) for _, v := range removed_targets { - fmt.Println("*", v) + r.logger.Println("*", v) } if len(t.Targets) != 0 { - fmt.Printf("[%v] Added/staged targets:\n", metaName) + r.logger.Printf("[%v] Added/staged targets:\n", metaName) for k := range t.Targets { - fmt.Println("*", k) + r.logger.Println("*", k) } } else { - fmt.Printf("[%v] There are no added/staged targets\n", metaName) + r.logger.Printf("[%v] There are no added/staged targets\n", metaName) } } return err @@ -1307,7 +1355,7 @@ func (r *Repo) SnapshotWithExpires(expires time.Time) error { } err = r.setMeta("snapshot.json", snapshot) if err == nil { - fmt.Println("Staged snapshot.json metadata with expiration date:", snapshot.Expires) + r.logger.Println("Staged snapshot.json metadata with expiration date:", snapshot.Expires) } return err } @@ -1339,7 +1387,7 @@ func (r *Repo) TimestampWithExpires(expires time.Time) error { err = r.setMeta("timestamp.json", timestamp) if err == nil { - fmt.Println("Staged timestamp.json metadata with expiration date:", timestamp.Expires) + r.logger.Println("Staged timestamp.json metadata with expiration date:", timestamp.Expires) } return err } @@ -1505,7 +1553,7 @@ func (r *Repo) Commit() error { err = r.local.Commit(root.ConsistentSnapshot, versions, hashes) if err == nil { - fmt.Println("Committed successfully") + r.logger.Println("Committed successfully") } return err } @@ -1513,7 +1561,7 @@ func (r *Repo) Commit() error { func (r *Repo) Clean() error { err := r.local.Clean() if err == nil { - fmt.Println("Removed all staged metadata and target files") + r.logger.Println("Removed all staged metadata and target files") } return err } diff --git a/repo_extended.go b/repo_extended.go new file mode 100644 index 00000000..795bf2fa --- /dev/null +++ b/repo_extended.go @@ -0,0 +1,99 @@ +package tuf + +import ( + "time" + + "github.com/theupdateframework/go-tuf/data" +) + +func (r *Repo) IncrementRootVersion() error { + return r.IncrementRootVersionWithExpires(data.DefaultExpires("root")) +} + +func (r *Repo) IncrementRootVersionWithExpires(expires time.Time) error { + role, err := r.root() + if err != nil { + return err + } + + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + role.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged("root.json") { + role.Version++ + } + + return r.setMeta("root.json", role) +} + +func (r *Repo) RootExpires() (time.Time, error) { + role, err := r.root() + if err != nil { + return time.Time{}, err + } + return role.Expires, nil +} + +func (r *Repo) IncrementSnapshotVersion() error { + return r.Snapshot() +} + +func (r *Repo) IncrementSnapshotVersionWithExpires(expires time.Time) error { + return r.SnapshotWithExpires(expires) +} + +func (r *Repo) SnapshotExpires() (time.Time, error) { + role, err := r.snapshot() + if err != nil { + return time.Time{}, err + } + return role.Expires, nil +} + +func (r *Repo) IncrementTargetsVersion() error { + return r.IncrementTargetsVersionWithExpires(data.DefaultExpires("targets")) +} + +func (r *Repo) IncrementTargetsVersionWithExpires(expires time.Time) error { + role, err := r.topLevelTargets() + if err != nil { + return err + } + + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + + role.Expires = expires.Round(time.Second) + if !r.local.FileIsStaged("targets.json") { + role.Version++ + } + + return r.setMeta("targets.json", role) +} + +func (r *Repo) TargetsExpires() (time.Time, error) { + role, err := r.topLevelTargets() + if err != nil { + return time.Time{}, err + } + return role.Expires, nil +} + +func (r *Repo) IncrementTimestampVersion() error { + return r.Timestamp() +} + +func (r *Repo) IncrementTimestampVersionWithExpires(expires time.Time) error { + return r.TimestampWithExpires(expires) +} + +func (r *Repo) TimestampExpires() (time.Time, error) { + role, err := r.timestamp() + if err != nil { + return time.Time{}, err + } + return role.Expires, nil +} diff --git a/repo_extended_test.go b/repo_extended_test.go new file mode 100644 index 00000000..f62ee34c --- /dev/null +++ b/repo_extended_test.go @@ -0,0 +1,249 @@ +package tuf + +import ( + . "gopkg.in/check.v1" +) + +func genKeyErr(_ []string, err error) error { + return err +} + +func initRepo(c *C, r *Repo, paths []string) { + c.Assert(r.Init(false), IsNil) + c.Assert(genKeyErr(r.GenKey("root")), IsNil) + c.Assert(genKeyErr(r.GenKey("targets")), IsNil) + c.Assert(genKeyErr(r.GenKey("snapshot")), IsNil) + c.Assert(genKeyErr(r.GenKey("timestamp")), IsNil) + c.Assert(r.AddTargets(paths, nil), IsNil) + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) +} + +func (RepoSuite) TestRolesExpirationsRotation(c *C) { + local := MemoryStore(nil, map[string][]byte{ + "myfile": []byte("DATA\n"), + }) + + r, err := NewRepo(local) + c.Assert(err, IsNil) + + initRepo(c, r, []string{"myfile"}) + + prevRootVersion, err := r.RootVersion() + c.Assert(err, IsNil) + prevRootExpires, err := r.RootExpires() + c.Assert(err, IsNil) + + prevTargetsVersion, err := r.TargetsVersion() + c.Assert(err, IsNil) + prevTargetsExpires, err := r.TargetsExpires() + c.Assert(err, IsNil) + + prevSnapshotVersion, err := r.SnapshotVersion() + c.Assert(err, IsNil) + prevSnapshotExpires, err := r.SnapshotExpires() + c.Assert(err, IsNil) + + prevTimestampVersion, err := r.TimestampVersion() + c.Assert(err, IsNil) + prevTimestampExpires, err := r.TimestampExpires() + c.Assert(err, IsNil) + + // Update timestamp + for i := 0; i < 10; i++ { + r, err := NewRepo(local) + c.Assert(err, IsNil) + + newTimestampExpires := prevTimestampExpires.AddDate(0, 0, 1) + + c.Assert(r.IncrementTimestampVersionWithExpires(newTimestampExpires), IsNil) + c.Assert(r.Commit(), IsNil) + + root, err := r.root() + c.Assert(err, IsNil) + c.Assert(root.Version, Equals, prevRootVersion) + c.Assert(root.Expires, Equals, prevRootExpires) + + targets, err := r.topLevelTargets() + c.Assert(err, IsNil) + c.Assert(targets.Version, Equals, prevTargetsVersion) + c.Assert(targets.Expires, Equals, prevTargetsExpires) + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Version, Equals, prevSnapshotVersion) + c.Assert(snapshot.Expires, Equals, prevSnapshotExpires) + + timestamp, err := r.timestamp() + c.Assert(err, IsNil) + c.Assert(timestamp.Version, Equals, prevTimestampVersion+1) + c.Assert(timestamp.Expires, Equals, newTimestampExpires) + prevTimestampVersion = timestamp.Version + prevTimestampExpires = timestamp.Expires + } + + // Update snapshot, depends on timestamp + for i := 0; i < 10; i++ { + r, err := NewRepo(local) + c.Assert(err, IsNil) + + newSnapshotExpires := prevSnapshotExpires.AddDate(0, 0, 7) + newTimestampExpires := prevTimestampExpires.AddDate(0, 0, 1) + + c.Assert(r.IncrementSnapshotVersionWithExpires(newSnapshotExpires), IsNil) + c.Assert(r.IncrementTimestampVersionWithExpires(newTimestampExpires), IsNil) + c.Assert(r.Commit(), IsNil) + + root, err := r.root() + c.Assert(err, IsNil) + c.Assert(root.Version, Equals, prevRootVersion) + c.Assert(root.Expires, Equals, prevRootExpires) + + targets, err := r.topLevelTargets() + c.Assert(err, IsNil) + c.Assert(targets.Version, Equals, prevTargetsVersion) + c.Assert(targets.Expires, Equals, prevTargetsExpires) + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Version, Equals, prevSnapshotVersion+1) + c.Assert(snapshot.Expires, Equals, newSnapshotExpires) + prevSnapshotVersion = snapshot.Version + prevSnapshotExpires = snapshot.Expires + + timestamp, err := r.timestamp() + c.Assert(err, IsNil) + c.Assert(timestamp.Version, Equals, prevTimestampVersion+1) + c.Assert(timestamp.Expires, Equals, newTimestampExpires) + prevTimestampVersion = timestamp.Version + prevTimestampExpires = timestamp.Expires + } + + // Update targets, depends on snapshot and timestamp + for i := 0; i < 10; i++ { + r, err := NewRepo(local) + c.Assert(err, IsNil) + + newTargetsExpires := prevTargetsExpires.AddDate(0, 3, 0) + newSnapshotExpires := prevSnapshotExpires.AddDate(0, 0, 7) + newTimestampExpires := prevTimestampExpires.AddDate(0, 0, 1) + + c.Assert(r.IncrementTargetsVersionWithExpires(newTargetsExpires), IsNil) + c.Assert(r.IncrementSnapshotVersionWithExpires(newSnapshotExpires), IsNil) + c.Assert(r.IncrementTimestampVersionWithExpires(newTimestampExpires), IsNil) + c.Assert(r.Commit(), IsNil) + + root, err := r.root() + c.Assert(err, IsNil) + c.Assert(root.Version, Equals, prevRootVersion) + c.Assert(root.Expires, Equals, prevRootExpires) + + targets, err := r.topLevelTargets() + c.Assert(err, IsNil) + c.Assert(targets.Version, Equals, prevTargetsVersion+1) + c.Assert(targets.Expires, Equals, newTargetsExpires) + prevTargetsVersion = targets.Version + prevTargetsExpires = targets.Expires + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Version, Equals, prevSnapshotVersion+1) + c.Assert(snapshot.Expires, Equals, newSnapshotExpires) + prevSnapshotVersion = snapshot.Version + prevSnapshotExpires = snapshot.Expires + + timestamp, err := r.timestamp() + c.Assert(err, IsNil) + c.Assert(timestamp.Version, Equals, prevTimestampVersion+1) + c.Assert(timestamp.Expires, Equals, newTimestampExpires) + prevTimestampVersion = timestamp.Version + prevTimestampExpires = timestamp.Expires + } + + // Update root, depends on snapshot and timestamp + for i := 0; i < 10; i++ { + r, err := NewRepo(local) + c.Assert(err, IsNil) + + newRootExpires := prevRootExpires.AddDate(1, 0, 0) + newSnapshotExpires := prevSnapshotExpires.AddDate(0, 0, 7) + newTimestampExpires := prevTimestampExpires.AddDate(0, 0, 1) + + c.Assert(r.IncrementRootVersionWithExpires(newRootExpires), IsNil) + c.Assert(r.IncrementSnapshotVersionWithExpires(newSnapshotExpires), IsNil) + c.Assert(r.IncrementTimestampVersionWithExpires(newTimestampExpires), IsNil) + c.Assert(r.Commit(), IsNil) + + root, err := r.root() + c.Assert(err, IsNil) + c.Assert(root.Version, Equals, prevRootVersion+1) + c.Assert(root.Expires, Equals, newRootExpires) + prevRootVersion = root.Version + prevRootExpires = root.Expires + + targets, err := r.topLevelTargets() + c.Assert(err, IsNil) + c.Assert(targets.Version, Equals, prevTargetsVersion) + c.Assert(targets.Expires, Equals, prevTargetsExpires) + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Version, Equals, prevSnapshotVersion+1) + c.Assert(snapshot.Expires, Equals, newSnapshotExpires) + prevSnapshotVersion = snapshot.Version + prevSnapshotExpires = snapshot.Expires + + timestamp, err := r.timestamp() + c.Assert(err, IsNil) + c.Assert(timestamp.Version, Equals, prevTimestampVersion+1) + c.Assert(timestamp.Expires, Equals, newTimestampExpires) + prevTimestampVersion = timestamp.Version + prevTimestampExpires = timestamp.Expires + } + + // Update root, targets, snapshot and timestamp at the same time + for i := 0; i < 10; i++ { + r, err := NewRepo(local) + c.Assert(err, IsNil) + + newRootExpires := prevRootExpires.AddDate(1, 0, 0) + newTargetsExpires := prevTargetsExpires.AddDate(0, 3, 0) + newSnapshotExpires := prevSnapshotExpires.AddDate(0, 0, 7) + newTimestampExpires := prevTimestampExpires.AddDate(0, 0, 1) + + c.Assert(r.IncrementRootVersionWithExpires(newRootExpires), IsNil) + c.Assert(r.IncrementTargetsVersionWithExpires(newTargetsExpires), IsNil) + c.Assert(r.IncrementSnapshotVersionWithExpires(newSnapshotExpires), IsNil) + c.Assert(r.IncrementTimestampVersionWithExpires(newTimestampExpires), IsNil) + c.Assert(r.Commit(), IsNil) + + root, err := r.root() + c.Assert(err, IsNil) + c.Assert(root.Version, Equals, prevRootVersion+1) + c.Assert(root.Expires, Equals, newRootExpires) + prevRootVersion = root.Version + prevRootExpires = root.Expires + + targets, err := r.topLevelTargets() + c.Assert(err, IsNil) + c.Assert(targets.Version, Equals, prevTargetsVersion+1) + c.Assert(targets.Expires, Equals, newTargetsExpires) + prevTargetsVersion = targets.Version + prevTargetsExpires = targets.Expires + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Version, Equals, prevSnapshotVersion+1) + c.Assert(snapshot.Expires, Equals, newSnapshotExpires) + prevSnapshotVersion = snapshot.Version + prevSnapshotExpires = snapshot.Expires + + timestamp, err := r.timestamp() + c.Assert(err, IsNil) + c.Assert(timestamp.Version, Equals, prevTimestampVersion+1) + c.Assert(timestamp.Expires, Equals, newTimestampExpires) + prevTimestampVersion = timestamp.Version + prevTimestampExpires = timestamp.Expires + } +} diff --git a/repo_test.go b/repo_test.go index 2f3aebb4..ad69e664 100644 --- a/repo_test.go +++ b/repo_test.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "os" "path" "path/filepath" @@ -1422,7 +1423,12 @@ func (rs *RepoSuite) TestKeyPersistence(c *C) { // Test changing the passphrase // 1. Create a secure store with a passphrase (create new object and temp folder so we discard any previous state) tmp = newTmpDir(c) - store = FileSystemStore(tmp.path, testPassphraseFunc) + var logBytes bytes.Buffer + storeOpts := StoreOpts{ + Logger: log.New(&logBytes, "", 0), + PassFunc: testPassphraseFunc, + } + store = FileSystemStoreWithOpts(tmp.path, storeOpts) // 1.5. Changing passphrase works for top-level and delegated roles. r, err := NewRepo(store) @@ -1433,6 +1439,7 @@ func (rs *RepoSuite) TestKeyPersistence(c *C) { // 2. Test changing the passphrase when the keys file does not exist - should FAIL c.Assert(store.(PassphraseChanger).ChangePassphrase("root"), NotNil) + c.Assert(strings.Contains(logBytes.String(), "Missing keys file"), Equals, true) // 3. Generate a new key signer, err = keys.GenerateEd25519Key() @@ -2037,6 +2044,14 @@ func (rs *RepoSuite) TestDelegations(c *C) { t, err := r.targets(delegator) c.Assert(err, IsNil) + // Check if there are no delegations. + if t.Delegations == nil { + if delegatedRoles != nil { + c.Fatal("expected delegated roles on delegator") + } + return + } + // Check that delegated roles are copied verbatim. c.Assert(t.Delegations.Roles, DeepEquals, delegatedRoles) @@ -2648,7 +2663,8 @@ func (rs *RepoSuite) TestSnapshotWithInvalidRoot(c *C) { local.SetMeta("root.json", b) // Snapshotting should fail. - c.Assert(r.Snapshot(), Equals, ErrInsufficientSignatures{"root.json", verify.ErrInvalid}) + c.Assert(r.Snapshot(), Equals, ErrInsufficientSignatures{ + "root.json", verify.ErrRoleThreshold{Expected: 1, Actual: 0}}) // Correctly sign root c.Assert(r.Sign("root.json"), IsNil) diff --git a/requirements-test.txt b/requirements-test.txt index a364c2d7..00f20734 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,5 @@ -iso8601==1.0.2 -requests==2.28.1 -securesystemslib==0.22.0 +iso8601==1.1.0 +requests==2.28.2 +securesystemslib==0.26.0 six==1.16.0 -tuf==1.0.0 \ No newline at end of file +tuf==2.0.0 diff --git a/verify/verify.go b/verify/verify.go index f5675a25..e62042ee 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -8,6 +8,7 @@ import ( "github.com/secure-systems-lab/go-securesystemslib/cjson" "github.com/theupdateframework/go-tuf/data" "github.com/theupdateframework/go-tuf/internal/roles" + "github.com/theupdateframework/go-tuf/pkg/keys" ) type signedMeta struct { @@ -16,6 +17,22 @@ type signedMeta struct { Version int64 `json:"version"` } +// VerifySignature takes a signed JSON message, a signature, and a +// verifier and verifies the given signature on the JSON message +// using the verifier. It returns an error if verification fails. +func VerifySignature(signed json.RawMessage, sig data.HexBytes, + verifier keys.Verifier) error { + var decoded map[string]interface{} + if err := json.Unmarshal(signed, &decoded); err != nil { + return err + } + msg, err := cjson.EncodeCanonical(decoded) + if err != nil { + return err + } + return verifier.Verify(msg, sig) +} + func (db *DB) VerifyIgnoreExpiredCheck(s *data.Signed, role string, minVersion int64) error { if err := db.VerifySignatures(s, role); err != nil { return err @@ -80,15 +97,6 @@ func (db *DB) VerifySignatures(s *data.Signed, role string) error { return ErrUnknownRole{role} } - var decoded map[string]interface{} - if err := json.Unmarshal(s.Signed, &decoded); err != nil { - return err - } - msg, err := cjson.EncodeCanonical(decoded) - if err != nil { - return err - } - // Verify that a threshold of keys signed the data. Since keys can have // multiple key ids, we need to protect against multiple attached // signatures that just differ on the key id. @@ -103,9 +111,13 @@ func (db *DB) VerifySignatures(s *data.Signed, role string) error { continue } - if err := verifier.Verify(msg, sig.Signature); err != nil { - // FIXME: don't err out on the 1st bad signature. - return ErrInvalid + if err := VerifySignature(s.Signed, sig.Signature, verifier); err != nil { + // If a signature fails verification, don't count it towards the + // threshold but also return early and error out immediately. + // Note: Because of this, it is impossible to distinguish between + // an error of an invalid signature and a threshold not achieved. + // Invalid signatures lead to not achieving the threshold. + continue } // Only consider this key valid if we haven't seen any of it's diff --git a/verify/verify_test.go b/verify/verify_test.go index afbf79d8..191c1ed0 100644 --- a/verify/verify_test.go +++ b/verify/verify_test.go @@ -89,9 +89,12 @@ func (VerifySuite) Test(c *C) { err: ErrUnknownRole{"foo"}, }, { + // It is impossible to distinguish between an error of an invalid + // signature and a threshold not achieved. Invalid signatures lead + // to not achieving the threshold. name: "signature wrong length", mut: func(t *test) { t.s.Signatures[0].Signature = []byte{0} }, - err: ErrInvalid, + err: ErrRoleThreshold{1, 0}, }, { name: "key missing from role", @@ -101,7 +104,15 @@ func (VerifySuite) Test(c *C) { { name: "invalid signature", mut: func(t *test) { t.s.Signatures[0].Signature = make([]byte, ed25519.SignatureSize) }, - err: ErrInvalid, + err: ErrRoleThreshold{1, 0}, + }, + { + name: "enough signatures with extra invalid signature", + mut: func(t *test) { + t.s.Signatures = append(t.s.Signatures, data.Signature{ + KeyID: t.s.Signatures[0].KeyID, + Signature: make([]byte, ed25519.SignatureSize)}) + }, }, { name: "not enough signatures", @@ -189,7 +200,8 @@ func (VerifySuite) Test(c *C) { }, }, { - name: "invalid ecdsa signature", + // The threshold is still achieved. + name: "invalid second ecdsa signature", mut: func(t *test) { k, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) s := ecdsaSigner{k} @@ -198,7 +210,6 @@ func (VerifySuite) Test(c *C) { t.keys = append(t.keys, s.PublicData()) t.roles["root"].KeyIDs = append(t.roles["root"].KeyIDs, s.PublicData().IDs()...) }, - err: ErrInvalid, }, } for _, t := range tests {