Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update TUF client to support options and add LiveTrustedRoot #41

Merged
merged 35 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
444736a
Update TUF client to support options and add LiveTrustedRoot
codysoyland Dec 11, 2023
035c084
Make sure DefaultOptions never fails
kommendorkapten Dec 22, 2023
5f4fafa
avoid empty strings for arguments, use named attributes
kommendorkapten Dec 22, 2023
511a0b9
Ignore emacs backup files
kommendorkapten Dec 22, 2023
e680b4f
Created a bascig config file for the tuf client
kommendorkapten Dec 22, 2023
7be28c0
Style fixes
kommendorkapten Dec 22, 2023
780beb3
Made consistent snapshot configurable
kommendorkapten Dec 22, 2023
b1f195f
Clarified the use of unsafe local mode
kommendorkapten Dec 22, 2023
3e2ab65
Updated to go-tuf/v2@master
kommendorkapten Jan 29, 2024
8297aeb
Resolved merge conflict
kommendorkapten Jan 29, 2024
9c4e1c4
Merge branch 'main' into tuf-client-2
kommendorkapten Jan 29, 2024
11dedbd
Fixed errors from linter
kommendorkapten Jan 29, 2024
dc7e979
Use short variable declaration syntax
codysoyland Jan 29, 2024
8bc63cf
Remove old unused embedded root
codysoyland Jan 29, 2024
c270ed8
Add func to fetch TUF root with given options
codysoyland Jan 29, 2024
a8fd9e0
Add chainable functional options to Options struct
codysoyland Jan 29, 2024
95168ba
Update CodeQL action
codysoyland Jan 29, 2024
4f6cb84
Setup Go version in CodeQL workflwo
codysoyland Jan 29, 2024
c2e715e
Don't specify minor go version
kommendorkapten Jan 30, 2024
e87063c
Added a simple test for an offline cliant
kommendorkapten Jan 30, 2024
96326fa
Add TUF repo creation and basic test to create a client
codysoyland Feb 5, 2024
057aa83
Made the tuf root file configurable via the command line
kommendorkapten Feb 6, 2024
4ac2b31
Merge branch 'main' into tuf-client-2
kommendorkapten Feb 6, 2024
72edefd
Use consts from go-tuf
codysoyland Feb 6, 2024
0def807
Add test to fetch target
codysoyland Feb 6, 2024
f4d0556
Breakout publish
codysoyland Feb 7, 2024
ee12af4
Add target support and refresh test
codysoyland Feb 7, 2024
fe78b34
Add TUF caching tests
codysoyland Feb 7, 2024
651aff1
Remove unreachable code, add more tests
codysoyland Feb 7, 2024
fd475da
Updated go-tuf
kommendorkapten Feb 9, 2024
616ee98
Updated to latest go-tuf
kommendorkapten Feb 9, 2024
51aaf34
Clarified that the updates is replaced, not the actual tuf client
kommendorkapten Feb 9, 2024
10be16d
Updated to new error type (pointer)
kommendorkapten Feb 9, 2024
1d0f156
Use 0 days for default CacheValidity
codysoyland Feb 9, 2024
37bb81f
Clarify CacheValidity option and add NoCache/MaxCache consts
codysoyland Feb 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions pkg/tuf/client.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add tests?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think so, I'll work on getting some in.

Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,7 @@ func (c *Client) loadMetadata() error {
cfg = &Config{}
}

cacheValidUntil := cfg.LastTimestamp.Add(
time.Duration(-24*c.opts.CacheValidity) * time.Hour)
cacheValidUntil := cfg.LastTimestamp.AddDate(0, 0, c.opts.CacheValidity)
if time.Now().Before(cacheValidUntil) {
// No need to update
return nil
Expand Down Expand Up @@ -183,12 +182,12 @@ func (c *Client) GetTarget(target string) ([]byte, error) {
const filePath = ""
ti, err := c.up.GetTargetInfo(target)
if err != nil {
return nil, fmt.Errorf("target %s not found: %w", target, err)
return nil, fmt.Errorf("getting info for target \"%s\": %w", target, err)
}

path, tb, err := c.up.FindCachedTarget(ti, filePath)
if err != nil {
return nil, fmt.Errorf("error getting target cache: %w", err)
return nil, fmt.Errorf("getting target cache: %w", err)
}
if path != "" {
// Cached version found
Expand Down
180 changes: 164 additions & 16 deletions pkg/tuf/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestNewOfflineClientFail(t *testing.T) {

func TestRefresh(t *testing.T) {
r := newTestRepo(t)
r.AddTarget("foo", []byte("foo 1"))
r.AddTarget("foo", []byte("foo version 1"))
rootJSON, err := r.roles.Root().ToBytes(false)
if err != nil {
t.Fatal(err)
Expand All @@ -70,17 +70,140 @@ func TestRefresh(t *testing.T) {
target, err := c.GetTarget("foo")
assert.NoError(t, err)
assert.NotNil(t, target)
assert.Equal(t, target, []byte("foo 1"))
assert.Equal(t, target, []byte("foo version 1"))

r.AddTarget("foo", []byte("foo 2"))
r.AddTarget("foo", []byte("foo version 2"))
assert.NoError(t, c.Refresh())

target, err = c.GetTarget("foo")
assert.NoError(t, err)
assert.NotNil(t, target)
assert.Equal(t, target, []byte("foo 2"))
assert.Equal(t, target, []byte("foo version 2"))
}

func TestCache(t *testing.T) {
r := newTestRepo(t)
r.AddTarget("foo", []byte("foo version 1"))
rootJSON, err := r.roles.Root().ToBytes(false)
if err != nil {
t.Fatal(err)
}

var opt = DefaultOptions().
WithRepositoryBaseURL("https://testing.local").
WithRoot(rootJSON).
WithCachePath(t.TempDir()).
WithFetcher(r).
WithCacheValidity(1)

c, err := New(opt)
assert.NotNil(t, c)
assert.NoError(t, err)

target, err := c.GetTarget("foo")
assert.NoError(t, err)
assert.NotNil(t, target)
assert.Equal(t, target, []byte("foo version 1"))

r.AddTarget("foo", []byte("foo version 2"))

// Create new client with the same cache path
c, err = New(opt)
assert.NotNil(t, c)
assert.NoError(t, err)

target, err = c.GetTarget("foo")
assert.NoError(t, err)
assert.NotNil(t, target)
// Cache is still valid, so we should get the old version
assert.Equal(t, target, []byte("foo version 1"))

// Set last updated time to 2 days ago, to trigger cache refresh
cfg, err := LoadConfig(c.configPath())
if err != nil {
t.Fatal(err)
}
cfg.LastTimestamp = time.Now().Add(-48 * time.Hour)
err = cfg.Persist(c.configPath())
if err != nil {
t.Fatal(err)
}

// Create new client with the same cache path
c, err = New(opt)
assert.NotNil(t, c)
assert.NoError(t, err)

// Now we should get the new version
target, err = c.GetTarget("foo")
assert.NoError(t, err)
assert.Equal(t, target, []byte("foo version 2"))
}

func TestExpiredTimestamp(t *testing.T) {
r := newTestRepo(t)
r.AddTarget("foo", []byte("foo version 1"))
rootJSON, err := r.roles.Root().ToBytes(false)
if err != nil {
t.Fatal(err)
}

var opt = DefaultOptions().
WithRepositoryBaseURL("https://testing.local").
WithRoot(rootJSON).
WithCachePath(t.TempDir()).
WithFetcher(r).
WithCacheValidity(1)

c, err := New(opt)
assert.NotNil(t, c)
assert.NoError(t, err)

target, err := c.GetTarget("foo")
assert.NoError(t, err)
assert.Equal(t, target, []byte("foo version 1"))

r.AddTarget("foo", []byte("foo version 2"))

opt.ForceCache = true
c, err = New(opt)
assert.NotNil(t, c)
assert.NoError(t, err)

target, err = c.GetTarget("foo")
assert.NoError(t, err)
// Using ForceCache, so we should get the old version
assert.Equal(t, target, []byte("foo version 1"))

r.SetTimestamp(time.Now())

// Manually write timestamp to disk, as Refresh() will fail
err = r.roles.Timestamp().ToFile(filepath.Join(opt.CachePath, "testing.local", "timestamp.json"), false)
if err != nil {
t.Fatal(err)
}

// Client creation should fail as the timestamp is expired and the repository has an expired timestamp
c, err = New(opt)
assert.Nil(t, c)
assert.Error(t, err)

// Update repo with unexpired timestamp
r.SetTimestamp(time.Now().AddDate(0, 0, 1))

c, err = New(opt)
assert.NotNil(t, c)
assert.NoError(t, err)

target, err = c.GetTarget("foo")
assert.NoError(t, err)
// Even though ForceCache is set, we should get the new version since the cached timestamp is expired
assert.Equal(t, target, []byte("foo version 2"))
}

// repo represents repositoryType from
// github.com/theupdateframework/go-tuf/v2/metadata/repository, which is
// unexported.
type repo interface {
Root() *metadata.Metadata[metadata.RootType]
SetRoot(meta *metadata.Metadata[metadata.RootType])
Expand All @@ -91,21 +214,28 @@ type repo interface {
Targets(name string) *metadata.Metadata[metadata.TargetsType]
SetTargets(name string, meta *metadata.Metadata[metadata.TargetsType])
}
type testrepo struct {

// testRepo is a basic implementation of a TUF repository for testing purposes.
// It does not support delegates, multiple signers, thresholds, or other
// advanced TUF features, but it is sufficient for testing the sigstore-go
// client. Those other features should be covered by the go-tuf tests. This is
// primarily intended to test the caching and fetching behavior of the client.
type testRepo struct {
keys map[string]ed25519.PrivateKey
roles repo
dir string
t *testing.T
}

func newTestRepo(t *testing.T) *testrepo {
func newTestRepo(t *testing.T) *testRepo {
var err error
r := &testrepo{
r := &testRepo{
keys: make(map[string]ed25519.PrivateKey),
roles: repository.New(),
t: t,
}
targets := metadata.Targets(helperExpireIn(7))
tomorrow := time.Now().AddDate(0, 0, 1).UTC()
targets := metadata.Targets(tomorrow)
r.roles.SetTargets(metadata.TARGETS, targets)
r.dir, err = os.MkdirTemp("", "tuf-test-repo")
if err != nil {
Expand All @@ -115,11 +245,11 @@ func newTestRepo(t *testing.T) *testrepo {
if err != nil {
t.Fatal(err)
}
snapshot := metadata.Snapshot(helperExpireIn(7))
snapshot := metadata.Snapshot(tomorrow)
r.roles.SetSnapshot(snapshot)
timestamp := metadata.Timestamp(helperExpireIn(1))
timestamp := metadata.Timestamp(tomorrow)
r.roles.SetTimestamp(timestamp)
root := metadata.Root(helperExpireIn(365))
root := metadata.Root(tomorrow)
r.roles.SetRoot(root)

for _, name := range []string{metadata.TARGETS, metadata.SNAPSHOT, metadata.TIMESTAMP, metadata.ROOT} {
Expand Down Expand Up @@ -162,7 +292,10 @@ func newTestRepo(t *testing.T) *testrepo {
return r
}

func (r *testrepo) DownloadFile(urlPath string, _ int64, _ time.Duration) ([]byte, error) {
// DownloadFile is a test implementation of the Fetcher interface, which the
// client may use to avoid making real HTTP requests. It returns the contents
// of the metadata files and target files in the test repository.
func (r *testRepo) DownloadFile(urlPath string, _ int64, _ time.Duration) ([]byte, error) {
u, err := url.Parse(urlPath)
if err != nil {
return []byte{}, err
Expand Down Expand Up @@ -223,7 +356,10 @@ func (r *testrepo) DownloadFile(urlPath string, _ int64, _ time.Duration) ([]byt
return []byte{}, nil
}

func (r *testrepo) AddTarget(name string, content []byte) {
// AddTarget adds a target file to the repository. It also creates a new
// snapshot and timestamp metadata file, and signs them with the appropriate
// key.
func (r *testRepo) AddTarget(name string, content []byte) {
targetHash := sha256.Sum256(content)
localPath := filepath.Join(r.dir, metadata.TARGETS, fmt.Sprintf("%x.%s", targetHash, name))
err := os.WriteFile(localPath, content, 0600)
Expand Down Expand Up @@ -265,7 +401,19 @@ func (r *testrepo) AddTarget(name string, content []byte) {
}
}

// helperExpireIn returns time offset by days
func helperExpireIn(days int) time.Time {
return time.Now().AddDate(0, 0, days).UTC()
// SetTimestamp sets the expiration date of the timestamp metadata file to the
// given date, and increments the version number. It then signs the metadata
// file with the appropriate key.
func (r *testRepo) SetTimestamp(date time.Time) {
r.roles.Timestamp().Signed.Expires = date
r.roles.Timestamp().Signed.Version++
signer, err := signature.LoadSigner(r.keys[metadata.TIMESTAMP], crypto.Hash(0))
if err != nil {
r.t.Fatal(err)
}
r.roles.Timestamp().ClearSignatures()
_, err = r.roles.Timestamp().Sign(signer)
if err != nil {
r.t.Fatal(err)
}
}
2 changes: 1 addition & 1 deletion pkg/tuf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (c *Config) Persist(p string) error {
if err != nil {
return fmt.Errorf("failed to JSON marshal config: %w", err)
}
err = os.WriteFile(p, b, 0400) // Read only by current user
err = os.WriteFile(p, b, 0600)
kommendorkapten marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
Expand Down
Loading