File: cache.go

package info (click to toggle)
gittuf 0.12.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,692 kB
  • sloc: python: 85; makefile: 58; sh: 1
file content (234 lines) | stat: -rw-r--r-- 6,229 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package cache

import (
	"encoding/json"
	"errors"
	"log/slog"

	"github.com/gittuf/gittuf/internal/attestations"
	"github.com/gittuf/gittuf/internal/gitinterface"
	"github.com/gittuf/gittuf/internal/rsl"
)

const (
	Ref = "refs/local/gittuf/persistent-cache"

	persistentTreeEntryName = "persistentCache"

	policyRef = "refs/gittuf/policy" // this is copied from internal/policy to avoid an import cycle
)

var (
	ErrNoPersistentCache = errors.New("persistent cache not found")
	ErrEntryNotNumbered  = errors.New("one or more entries are not numbered")
)

type Persistent struct {
	// PolicyEntries is a list of index values for entries pertaining to the
	// policy ref. The list is ordered by each entry's Number.
	PolicyEntries []RSLEntryIndex `json:"policyEntries"`

	// AttestationEntries is a list of index values for entries pertaining to
	// the attestations ref. The list is ordered by each entry's Number.
	AttestationEntries []RSLEntryIndex `json:"attestationEntries"`

	// AddedAttestationsBeforeNumber tracks the number up to which
	// attestations have been searched for and added to
	// attestationsEntryNumbers. We need to track this for attestations in
	// particular because attestations are optional in gittuf repositories,
	// meaning attestationsEntryNumbers may be empty which would trigger a
	// full search.
	AddedAttestationsBeforeNumber uint64 `json:"addedAttestationsBeforeNumber"`

	// LastVerifiedEntryForRef is a map that indicates the last verified RSL
	// entry for a ref.
	LastVerifiedEntryForRef map[string]RSLEntryIndex `json:"lastVerifiedEntryForRef"`
}

func (p *Persistent) Commit(repo *gitinterface.Repository) error {
	if len(p.PolicyEntries) == 0 && len(p.AttestationEntries) == 0 && p.AddedAttestationsBeforeNumber == 0 && len(p.LastVerifiedEntryForRef) == 0 {
		// nothing to do
		return nil
	}

	contents, err := json.Marshal(p)
	if err != nil {
		return err
	}

	blobID, err := repo.WriteBlob(contents)
	if err != nil {
		return err
	}

	treeBuilder := gitinterface.NewTreeBuilder(repo)
	treeID, err := treeBuilder.WriteTreeFromEntries([]gitinterface.TreeEntry{gitinterface.NewEntryBlob(persistentTreeEntryName, blobID)})
	if err != nil {
		return err
	}

	currentCommitID, _ := repo.GetReference(Ref) //nolint:errcheck
	if !currentCommitID.IsZero() {
		currentTreeID, err := repo.GetCommitTreeID(currentCommitID)
		if err == nil && treeID.Equal(currentTreeID) {
			// no change in cache contents, noop
			return nil
		}
	}

	_, err = repo.Commit(treeID, Ref, "Set persistent cache\n", false)
	return err
}

// PopulatePersistentCache scans the repository's RSL and generates a persistent
// local-only cache of policy and attestation entries. This makes subsequent
// verifications faster.
func PopulatePersistentCache(repo *gitinterface.Repository) error {
	persistent := &Persistent{
		PolicyEntries:      []RSLEntryIndex{},
		AttestationEntries: []RSLEntryIndex{},
	}

	iterator, err := rsl.GetLatestEntry(repo)
	if err != nil {
		return err
	}

	if iterator.GetNumber() == 0 {
		return ErrEntryNotNumbered
	}

	persistent.AddedAttestationsBeforeNumber = iterator.GetNumber()

	for {
		if iterator, isReferenceEntry := iterator.(*rsl.ReferenceEntry); isReferenceEntry {
			switch iterator.RefName {
			case policyRef:
				persistent.InsertPolicyEntryNumber(iterator.GetNumber(), iterator.GetID())
			case attestations.Ref:
				persistent.InsertAttestationEntryNumber(iterator.GetNumber(), iterator.GetID())
			}
		}

		iterator, err = rsl.GetParentForEntry(repo, iterator)
		if err != nil {
			if errors.Is(err, rsl.ErrRSLEntryNotFound) {
				break
			}

			return err
		}

		if iterator.GetNumber() == 0 {
			return ErrEntryNotNumbered
		}
	}

	return persistent.Commit(repo)
}

// LoadPersistentCache loads the persistent cache from the tip of the local ref.
// If an instance has already been loaded and a pointer has been stored in
// memory, that instance is returned.
func LoadPersistentCache(repo *gitinterface.Repository) (*Persistent, error) {
	slog.Debug("Loading persistent cache from disk...")

	commitID, err := repo.GetReference(Ref)
	if err != nil {
		if errors.Is(err, gitinterface.ErrReferenceNotFound) {
			// Persistent cache doesn't exist
			slog.Debug("Persistent cache does not exist")
			return nil, ErrNoPersistentCache
		}

		return nil, err
	}

	treeID, err := repo.GetCommitTreeID(commitID)
	if err != nil {
		return nil, err
	}

	allFiles, err := repo.GetAllFilesInTree(treeID)
	if err != nil {
		return nil, err
	}

	blobID, has := allFiles[persistentTreeEntryName]
	if !has {
		// Persistent cache doesn't seem to exist? This maybe warrants
		// an error but we may have more than one file here in future?
		slog.Debug("Persistent cache does not exist")
		return nil, ErrNoPersistentCache
	}

	blob, err := repo.ReadBlob(blobID)
	if err != nil {
		return nil, err
	}

	persistentCache := &Persistent{}
	if err := json.Unmarshal(blob, &persistentCache); err != nil {
		return nil, err
	}

	slog.Debug("Loaded persistent cache")
	return persistentCache, nil
}

// DeletePersistentCache deletes the local persistent cache ref.
func DeletePersistentCache(repo *gitinterface.Repository) error {
	ref, err := repo.GetReference(Ref)
	if err != nil {
		if errors.Is(err, gitinterface.ErrReferenceNotFound) {
			return ErrNoPersistentCache
		}
		return err
	}

	if ref.IsZero() {
		return ErrNoPersistentCache
	}

	err = repo.DeleteReference(Ref)
	if err != nil {
		return err
	}

	return nil
}

// RSLEntryIndex is essentially a tuple that maps RSL entry IDs to numbers. This
// may be expanded in future to include more information as needed.
type RSLEntryIndex struct {
	EntryID     string `json:"entryID"`
	EntryNumber uint64 `json:"entryNumber"`
}

func (r *RSLEntryIndex) GetEntryID() gitinterface.Hash {
	hash, _ := gitinterface.NewHash(r.EntryID)
	// TODO: error?
	return hash
}

func (r *RSLEntryIndex) GetEntryNumber() uint64 {
	return r.EntryNumber
}

func binarySearch(a, b RSLEntryIndex) int {
	if a.GetEntryNumber() == b.GetEntryNumber() {
		// Exact match
		return 0
	}

	if a.GetEntryNumber() < b.GetEntryNumber() {
		// Precedes
		return -1
	}

	// Succeeds
	return 1
}