...

Source file src/github.com/sigstore/rekor/tests/harness_test.go

Documentation: github.com/sigstore/rekor/tests

     1  // Copyright 2022 The Sigstore Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  //go:build e2e
    16  // +build e2e
    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  // Make sure we can add an entry
    58  func TestHarnessAddEntry(t *testing.T) {
    59  	// Create a random artifact and sign it.
    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  	// Write the public key to a file
    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  	// Verify should fail initially
    75  	runCliErr(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
    76  
    77  	// It should upload successfully.
    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  		// Now we should be able to verify it.
    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  // Make sure we can add an intoto entry
    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  	// Get some random data so it's unique each run
    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  	// If we do it twice, it should already exist
   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  	// The attestation should be stored at /var/run/attestations/sha256:digest
   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  // Make sure we can get and verify all entries
   251  // For attestations, make sure we can see the attestation
   252  // Older versions of the CLI may not be able to parse the retrieved entry.
   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  // Check if we have a new server version and an old CLI version
   341  // since the new server returns an EntryID but the old CLI version expects a UUID
   342  // Also, new rekor server allows upload of intoto v0.0.2, and old rekor cli versions
   343  // don't understand how to parse these entries.
   344  // TODO: use semver comparisons.
   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