1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package e2e
19
20 import (
21 "bytes"
22 "context"
23 "crypto"
24 "crypto/ecdsa"
25 "crypto/sha256"
26 "crypto/x509"
27 "encoding/base64"
28 "encoding/hex"
29 "encoding/json"
30 "encoding/pem"
31 "errors"
32 "fmt"
33 "io/ioutil"
34 "os"
35 "path/filepath"
36 "strconv"
37 "strings"
38 "testing"
39
40 "github.com/google/go-cmp/cmp"
41 "github.com/in-toto/in-toto-golang/in_toto"
42 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
43 slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
44 "github.com/secure-systems-lab/go-securesystemslib/dsse"
45 "github.com/sigstore/rekor/pkg/generated/models"
46 sigx509 "github.com/sigstore/rekor/pkg/pki/x509"
47 "github.com/sigstore/rekor/pkg/sharding"
48 "github.com/sigstore/rekor/pkg/types"
49 "github.com/sigstore/sigstore/pkg/signature"
50 )
51
52 type StoredEntry struct {
53 Attestation string
54 UUID string
55 }
56
57
58 func TestHarnessAddEntry(t *testing.T) {
59
60 artifactPath := filepath.Join(t.TempDir(), "artifact")
61 sigPath := filepath.Join(t.TempDir(), "signature.asc")
62
63 sigx509.CreatedX509SignedArtifact(t, artifactPath, sigPath)
64 dataBytes, _ := ioutil.ReadFile(artifactPath)
65 h := sha256.Sum256(dataBytes)
66 dataSHA := hex.EncodeToString(h[:])
67
68
69 pubPath := filepath.Join(t.TempDir(), "pubKey.asc")
70 if err := ioutil.WriteFile(pubPath, []byte(sigx509.RSACert), 0644); err != nil {
71 t.Fatal(err)
72 }
73
74
75 runCliErr(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
76
77
78 out := runCli(t, "upload", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
79 outputContains(t, out, "Created entry at")
80 uuid := getUUIDFromUploadOutput(t, out)
81 logIndex := getLogIndexFromUploadOutput(t, out)
82
83 if !rekorCLIIncompatible() {
84
85 out = runCli(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
86 outputContains(t, out, "Inclusion Proof:")
87 }
88
89 saveEntry(t, logIndex, StoredEntry{UUID: uuid})
90 }
91
92
93 func TestHarnessAddIntoto(t *testing.T) {
94 td := t.TempDir()
95 attestationPath := filepath.Join(td, "attestation.json")
96 pubKeyPath := filepath.Join(td, "pub.pem")
97
98
99 d := randomData(t, 10)
100 id := base64.StdEncoding.EncodeToString(d)
101
102 it := in_toto.ProvenanceStatement{
103 StatementHeader: in_toto.StatementHeader{
104 Type: in_toto.StatementInTotoV01,
105 PredicateType: slsa.PredicateSLSAProvenance,
106 Subject: []in_toto.Subject{
107 {
108 Name: "foobar",
109 Digest: common.DigestSet{
110 "foo": "bar",
111 },
112 },
113 },
114 },
115 Predicate: slsa.ProvenancePredicate{
116 Builder: common.ProvenanceBuilder{
117 ID: "foo" + id,
118 },
119 },
120 }
121
122 b, err := json.Marshal(it)
123 if err != nil {
124 t.Fatal(err)
125 }
126
127 pb, _ := pem.Decode([]byte(sigx509.ECDSAPriv))
128 priv, err := x509.ParsePKCS8PrivateKey(pb.Bytes)
129 if err != nil {
130 t.Fatal(err)
131 }
132
133 s, err := signature.LoadECDSASigner(priv.(*ecdsa.PrivateKey), crypto.SHA256)
134 if err != nil {
135 t.Fatal(err)
136 }
137
138 signer, err := dsse.NewEnvelopeSigner(&sigx509.Verifier{
139 S: s,
140 })
141 if err != nil {
142 t.Fatal(err)
143 }
144
145 env, err := signer.SignPayload(context.Background(), "application/vnd.in-toto+json", b)
146 if err != nil {
147 t.Fatal(err)
148 }
149
150 eb, err := json.Marshal(env)
151 if err != nil {
152 t.Fatal(err)
153 }
154
155 write(t, string(eb), attestationPath)
156 write(t, sigx509.ECDSAPub, pubKeyPath)
157
158
159 out := runCliStdout(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
160 outputContains(t, out, "Created entry at")
161 uuid := getUUIDFromUploadOutput(t, out)
162 logIndex := getLogIndexFromUploadOutput(t, out)
163
164 out = runCli(t, "get", "--log-index", fmt.Sprintf("%d", logIndex), "--format=json")
165 g := getOut{}
166 if err := json.Unmarshal([]byte(out), &g); err != nil {
167 t.Fatal(err)
168 }
169
170
171 got := in_toto.ProvenanceStatement{}
172 if err := json.Unmarshal([]byte(g.Attestation), &got); err != nil {
173 t.Fatal(err)
174 }
175 if diff := cmp.Diff(it, got); diff != "" {
176 t.Errorf("diff: %s", diff)
177 }
178
179 attHash := sha256.Sum256(b)
180
181 intotoModel := &models.IntotoV002Schema{}
182 if err := types.DecodeEntry(g.Body.(map[string]interface{})["IntotoObj"], intotoModel); err != nil {
183 t.Errorf("could not convert body into intoto type: %v", err)
184 }
185 if intotoModel.Content == nil || intotoModel.Content.PayloadHash == nil {
186 t.Errorf("could not find hash over attestation %v", intotoModel)
187 }
188 recordedPayloadHash, err := hex.DecodeString(*intotoModel.Content.PayloadHash.Value)
189 if err != nil {
190 t.Errorf("error converting attestation hash to []byte: %v", err)
191 }
192
193 if !bytes.Equal(attHash[:], recordedPayloadHash) {
194 t.Fatal(fmt.Errorf("attestation hash %v doesnt match the payload we sent %v", hex.EncodeToString(attHash[:]),
195 *intotoModel.Content.PayloadHash.Value))
196 }
197
198 out = runCli(t, "upload", "--artifact", attestationPath, "--type", "intoto", "--public-key", pubKeyPath)
199 outputContains(t, out, "Entry already exists")
200 saveEntry(t, logIndex, StoredEntry{Attestation: g.Attestation, UUID: uuid})
201 }
202
203 func getEntries(t *testing.T) (string, map[int]StoredEntry) {
204 tmpDir := os.Getenv("REKOR_HARNESS_TMPDIR")
205 if tmpDir == "" {
206 t.Skip("Skipping test, REKOR_HARNESS_TMPDIR is not set")
207 }
208 file := filepath.Join(tmpDir, "attestations")
209
210 t.Log("Reading", file)
211 attestations := map[int]StoredEntry{}
212 contents, err := os.ReadFile(file)
213 if errors.Is(err, os.ErrNotExist) || contents == nil {
214 return file, attestations
215 }
216 if err != nil {
217 t.Fatal(err)
218 }
219 if err := json.Unmarshal(contents, &attestations); err != nil {
220 t.Fatal(err)
221 }
222 return file, attestations
223 }
224
225 func saveEntry(t *testing.T, logIndex int, entry StoredEntry) {
226 file, attestations := getEntries(t)
227 t.Logf("Storing entry for logIndex %d", logIndex)
228 attestations[logIndex] = entry
229 contents, err := json.Marshal(attestations)
230 if err != nil {
231 t.Fatal(err)
232 }
233 if err := os.WriteFile(file, contents, 0777); err != nil {
234 t.Fatal(err)
235 }
236 }
237
238 func compareAttestation(t *testing.T, logIndex int, got string) {
239 _, entries := getEntries(t)
240 expected, ok := entries[logIndex]
241 if !ok {
242 t.Fatalf("expected to find persisted entries with logIndex %d but none existed: %v", logIndex, entries)
243 }
244
245 if got != expected.Attestation {
246 t.Fatalf("attestations don't match, got %v expected %v", got, expected)
247 }
248 }
249
250
251
252
253 func TestHarnessGetAllEntriesLogIndex(t *testing.T) {
254 if rekorCLIIncompatible() {
255 t.Skipf("Skipping getting entries by UUID, old rekor-cli version %s is incompatible with server version %s", os.Getenv("CLI_VERSION"), os.Getenv("SERVER_VERSION"))
256 }
257
258 treeSize := activeTreeSize(t)
259 if treeSize == 0 {
260 t.Fatal("There are 0 entries in the log, there should be at least 2")
261 }
262 for i := 0; i < treeSize; i++ {
263 out := runCli(t, "get", "--log-index", fmt.Sprintf("%d", i), "--format", "json")
264 if !strings.Contains(out, "IntotoObj") {
265 continue
266 }
267 var intotoObj struct {
268 Attestation string
269 }
270 if err := json.Unmarshal([]byte(out), &intotoObj); err != nil {
271 t.Fatal(err)
272 }
273 compareAttestation(t, i, intotoObj.Attestation)
274 t.Log("IntotoObj matches stored attestation")
275 }
276 }
277
278 func TestHarnessGetAllEntriesUUID(t *testing.T) {
279 if rekorCLIIncompatible() {
280 t.Skipf("Skipping getting entries by UUID, old rekor-cli version %s is incompatible with server version %s", os.Getenv("CLI_VERSION"), os.Getenv("SERVER_VERSION"))
281 }
282
283 treeSize := activeTreeSize(t)
284 if treeSize == 0 {
285 t.Fatal("There are 0 entries in the log, there should be at least 2")
286 }
287 _, entries := getEntries(t)
288
289 for _, e := range entries {
290 outUUID := runCli(t, "get", "--uuid", e.UUID, "--format", "json")
291 outEntryID := runCli(t, "get", "--uuid", entryID(t, e.UUID), "--format", "json")
292
293 if outUUID != outEntryID {
294 t.Fatalf("Getting by uuid %s and entryID %s gave different outputs:\nuuid: %v\nentryID:%v\n", e.UUID, entryID(t, e.UUID), outUUID, outEntryID)
295 }
296
297 if !strings.Contains(outUUID, "IntotoObj") {
298 continue
299 }
300 var intotoObj struct {
301 Attestation string
302 }
303 if err := json.Unmarshal([]byte(outUUID), &intotoObj); err != nil {
304 t.Fatal(err)
305 }
306 if intotoObj.Attestation != e.Attestation {
307 t.Fatalf("attestations don't match, got %v expected %v", intotoObj.Attestation, e.Attestation)
308 }
309 }
310 }
311
312 func entryID(t *testing.T, uuid string) string {
313 if sharding.ValidateEntryID(uuid) == nil {
314 return uuid
315 }
316 treeID, err := strconv.Atoi(os.Getenv("TREE_ID"))
317 if err != nil {
318 t.Fatal(err)
319 }
320 tid := strconv.FormatInt(int64(treeID), 16)
321 ts, err := sharding.PadToTreeIDLen(tid)
322 if err != nil {
323 t.Fatal(err)
324 }
325 return ts + uuid
326 }
327
328 func activeTreeSize(t *testing.T) int {
329 out := runCliStdout(t, "loginfo", "--format", "json", "--store_tree_state", "false")
330 t.Log(string(out))
331 var s struct {
332 ActiveTreeSize int
333 }
334 if err := json.Unmarshal([]byte(out), &s); err != nil {
335 t.Fatal(err)
336 }
337 return s.ActiveTreeSize
338 }
339
340
341
342
343
344
345 func rekorCLIIncompatible() bool {
346 if sv := os.Getenv("SERVER_VERSION"); sv != "v0.10.0" && sv != "v0.11.0" {
347 if cv := os.Getenv("CLI_VERSION"); cv == "v0.10.0" || cv == "v0.11.0" {
348 return true
349 }
350 }
351
352 return false
353 }
354
View as plain text