...

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

Documentation: github.com/sigstore/rekor/tests

     1  //
     2  // Copyright 2021 The Sigstore Authors.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  
    16  //go:build e2e
    17  
    18  package e2e
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"crypto"
    24  	"crypto/ecdsa"
    25  	"crypto/sha256"
    26  	"encoding/hex"
    27  	"encoding/json"
    28  	"errors"
    29  	"fmt"
    30  	"io/ioutil"
    31  	"net/http"
    32  	"os"
    33  	"path/filepath"
    34  	"strconv"
    35  	"strings"
    36  	"testing"
    37  	"time"
    38  
    39  	"golang.org/x/sync/errgroup"
    40  
    41  	"cloud.google.com/go/pubsub"
    42  	"github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer"
    43  	"github.com/go-openapi/strfmt"
    44  	"github.com/go-openapi/swag"
    45  	"github.com/google/go-cmp/cmp"
    46  	"github.com/google/go-cmp/cmp/cmpopts"
    47  	"github.com/sigstore/rekor/pkg/client"
    48  	generatedClient "github.com/sigstore/rekor/pkg/generated/client"
    49  	"github.com/sigstore/rekor/pkg/generated/client/entries"
    50  	"github.com/sigstore/rekor/pkg/generated/client/pubkey"
    51  	"github.com/sigstore/rekor/pkg/generated/models"
    52  	sigx509 "github.com/sigstore/rekor/pkg/pki/x509"
    53  	"github.com/sigstore/rekor/pkg/sharding"
    54  	"github.com/sigstore/rekor/pkg/signer"
    55  	_ "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
    56  	rekord "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1"
    57  	"github.com/sigstore/sigstore/pkg/cryptoutils"
    58  	"github.com/sigstore/sigstore/pkg/signature"
    59  	"github.com/sigstore/sigstore/pkg/signature/options"
    60  )
    61  
    62  func getUUIDFromUploadOutput(t *testing.T, out string) string {
    63  	t.Helper()
    64  	// Output looks like "Artifact timestamped at ...\m Wrote response \n Created entry at index X, available at $URL/UUID", so grab the UUID:
    65  	urlTokens := strings.Split(strings.TrimSpace(out), " ")
    66  	url := urlTokens[len(urlTokens)-1]
    67  	splitUrl := strings.Split(url, "/")
    68  	return splitUrl[len(splitUrl)-1]
    69  }
    70  
    71  func getLogIndexFromUploadOutput(t *testing.T, out string) int {
    72  	t.Helper()
    73  	t.Log(out)
    74  	// Output looks like "Created entry at index X, available at $URL/UUID", so grab the index X:
    75  	split := strings.Split(strings.TrimSpace(out), ",")
    76  	ss := strings.Split(split[0], " ")
    77  	i, err := strconv.Atoi(ss[len(ss)-1])
    78  	if err != nil {
    79  		t.Fatal(err)
    80  	}
    81  	return i
    82  }
    83  
    84  func TestEnvVariableValidation(t *testing.T) {
    85  	os.Setenv("REKOR_FORMAT", "bogus")
    86  	defer os.Unsetenv("REKOR_FORMAT")
    87  
    88  	runCliErr(t, "loginfo")
    89  }
    90  
    91  func TestDuplicates(t *testing.T) {
    92  	artifactPath := filepath.Join(t.TempDir(), "artifact")
    93  	sigPath := filepath.Join(t.TempDir(), "signature.asc")
    94  
    95  	createdPGPSignedArtifact(t, artifactPath, sigPath)
    96  
    97  	// Write the public key to a file
    98  	pubPath := filepath.Join(t.TempDir(), "pubKey.asc")
    99  	if err := ioutil.WriteFile(pubPath, []byte(publicKey), 0644); err != nil {
   100  		t.Fatal(err)
   101  	}
   102  
   103  	// Now upload to rekor!
   104  	out := runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath, "--public-key", pubPath)
   105  	outputContains(t, out, "Created entry at")
   106  
   107  	// Now upload the same one again, we should get a dupe entry.
   108  	out = runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath, "--public-key", pubPath)
   109  	outputContains(t, out, "Entry already exists")
   110  
   111  	// Now do a new one, we should get a new entry
   112  	createdPGPSignedArtifact(t, artifactPath, sigPath)
   113  	out = runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath, "--public-key", pubPath)
   114  	outputContains(t, out, "Created entry at")
   115  }
   116  
   117  type getOut struct {
   118  	Attestation     string
   119  	AttestationType string
   120  	Body            interface{}
   121  	LogIndex        int
   122  	IntegratedTime  int64
   123  }
   124  
   125  func TestGetCLI(t *testing.T) {
   126  	// Create something and add it to the log
   127  	artifactPath := filepath.Join(t.TempDir(), "artifact")
   128  	sigPath := filepath.Join(t.TempDir(), "signature.asc")
   129  
   130  	createdPGPSignedArtifact(t, artifactPath, sigPath)
   131  
   132  	// Write the public key to a file
   133  	pubPath := filepath.Join(t.TempDir(), "pubKey.asc")
   134  	if err := ioutil.WriteFile(pubPath, []byte(publicKey), 0644); err != nil {
   135  		t.Fatal(err)
   136  	}
   137  	out := runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath, "--public-key", pubPath)
   138  	outputContains(t, out, "Created entry at")
   139  
   140  	uuid, err := sharding.GetUUIDFromIDString(getUUIDFromUploadOutput(t, out))
   141  	if err != nil {
   142  		t.Error(err)
   143  	}
   144  
   145  	// since we at least have 1 valid entry, check the log at index 0
   146  	runCli(t, "get", "--log-index", "0")
   147  
   148  	out = runCli(t, "get", "--format=json", "--uuid", uuid)
   149  
   150  	// The output here should be in JSON with this structure:
   151  	g := getOut{}
   152  	if err := json.Unmarshal([]byte(out), &g); err != nil {
   153  		t.Error(err)
   154  	}
   155  
   156  	if g.IntegratedTime == 0 {
   157  		t.Errorf("Expected IntegratedTime to be set. Got %s", out)
   158  	}
   159  	// Get it with the logindex as well
   160  	runCli(t, "get", "--format=json", "--log-index", strconv.Itoa(g.LogIndex))
   161  
   162  	// check index via the file and public key to ensure that the index has updated correctly
   163  	out = runCli(t, "search", "--artifact", artifactPath)
   164  	outputContains(t, out, uuid)
   165  
   166  	out = runCli(t, "search", "--public-key", pubPath)
   167  	outputContains(t, out, uuid)
   168  
   169  	artifactBytes, err := ioutil.ReadFile(artifactPath)
   170  	if err != nil {
   171  		t.Error(err)
   172  	}
   173  	sha := sha256.Sum256(artifactBytes)
   174  
   175  	out = runCli(t, "search", "--sha", fmt.Sprintf("sha256:%s", hex.EncodeToString(sha[:])))
   176  	outputContains(t, out, uuid)
   177  
   178  	// Exercise GET with the new EntryID (TreeID + UUID)
   179  	tid := getTreeID(t)
   180  	entryID, err := sharding.CreateEntryIDFromParts(fmt.Sprintf("%x", tid), uuid)
   181  	if err != nil {
   182  		t.Error(err)
   183  	}
   184  	runCli(t, "get", "--format=json", "--uuid", entryID.ReturnEntryIDString())
   185  }
   186  
   187  func publicKeyFromRekorClient(ctx context.Context, c *generatedClient.Rekor) (*ecdsa.PublicKey, error) {
   188  	resp, err := c.Pubkey.GetPublicKey(&pubkey.GetPublicKeyParams{Context: ctx})
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	// marshal the pubkey
   194  	pubKey, err := cryptoutils.UnmarshalPEMToPublicKey([]byte(resp.GetPayload()))
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	ed, ok := pubKey.(*ecdsa.PublicKey)
   199  	if !ok {
   200  		return nil, errors.New("public key retrieved from Rekor is not an ECDSA key")
   201  	}
   202  	return ed, nil
   203  }
   204  
   205  func TestSignedEntryTimestamp(t *testing.T) {
   206  	// Create a random payload and sign it
   207  	ctx := context.Background()
   208  	payload := []byte("payload")
   209  	s, err := signer.NewMemory()
   210  	if err != nil {
   211  		t.Fatal(err)
   212  	}
   213  	sig, err := s.SignMessage(bytes.NewReader(payload), options.WithContext(ctx))
   214  	if err != nil {
   215  		t.Fatal(err)
   216  	}
   217  	pubkey, err := s.PublicKey(options.WithContext(ctx))
   218  	if err != nil {
   219  		t.Fatal(err)
   220  	}
   221  	pemBytes, err := cryptoutils.MarshalPublicKeyToPEM(pubkey)
   222  	if err != nil {
   223  		t.Fatal(err)
   224  	}
   225  
   226  	// submit our newly signed payload to rekor
   227  	rekorClient, err := client.GetRekorClient(rekorServer())
   228  	if err != nil {
   229  		t.Fatal(err)
   230  	}
   231  
   232  	re := rekord.V001Entry{
   233  		RekordObj: models.RekordV001Schema{
   234  			Data: &models.RekordV001SchemaData{
   235  				Content: strfmt.Base64(payload),
   236  			},
   237  			Signature: &models.RekordV001SchemaSignature{
   238  				Content: (*strfmt.Base64)(&sig),
   239  				Format:  swag.String(models.RekordV001SchemaSignatureFormatX509),
   240  				PublicKey: &models.RekordV001SchemaSignaturePublicKey{
   241  					Content: (*strfmt.Base64)(&pemBytes),
   242  				},
   243  			},
   244  		},
   245  	}
   246  
   247  	returnVal := models.Rekord{
   248  		APIVersion: swag.String(re.APIVersion()),
   249  		Spec:       re.RekordObj,
   250  	}
   251  	params := entries.NewCreateLogEntryParams()
   252  	params.SetProposedEntry(&returnVal)
   253  	resp, err := rekorClient.Entries.CreateLogEntry(params)
   254  	if err != nil {
   255  		t.Fatal(err)
   256  	}
   257  	logEntry := extractLogEntry(t, resp.GetPayload())
   258  
   259  	// verify the signature against the log entry (without the signature)
   260  	timestampSig := logEntry.Verification.SignedEntryTimestamp
   261  	logEntry.Verification = nil
   262  	payload, err = logEntry.MarshalBinary()
   263  	if err != nil {
   264  		t.Fatal(err)
   265  	}
   266  	canonicalized, err := jsoncanonicalizer.Transform(payload)
   267  	if err != nil {
   268  		t.Fatal(err)
   269  	}
   270  	// get rekor's public key
   271  	rekorPubKey, err := publicKeyFromRekorClient(ctx, rekorClient)
   272  	if err != nil {
   273  		t.Fatal(err)
   274  	}
   275  
   276  	verifier, err := signature.LoadVerifier(rekorPubKey, crypto.SHA256)
   277  	if err != nil {
   278  		t.Fatal(err)
   279  	}
   280  	if err := verifier.VerifySignature(bytes.NewReader(timestampSig), bytes.NewReader(canonicalized), options.WithContext(ctx)); err != nil {
   281  		t.Fatal("unable to verify")
   282  	}
   283  }
   284  
   285  func TestEntryUpload(t *testing.T) {
   286  	artifactPath := filepath.Join(t.TempDir(), "artifact")
   287  	sigPath := filepath.Join(t.TempDir(), "signature.asc")
   288  
   289  	// Create the entry file
   290  	createdPGPSignedArtifact(t, artifactPath, sigPath)
   291  	payload, err := ioutil.ReadFile(artifactPath)
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  	sig, err := ioutil.ReadFile(sigPath)
   296  	if err != nil {
   297  		t.Fatal(err)
   298  	}
   299  
   300  	entryPath := filepath.Join(t.TempDir(), "entry.json")
   301  	pubKeyBytes := []byte(publicKey)
   302  
   303  	re := rekord.V001Entry{
   304  		RekordObj: models.RekordV001Schema{
   305  			Data: &models.RekordV001SchemaData{
   306  				Content: strfmt.Base64(payload),
   307  			},
   308  			Signature: &models.RekordV001SchemaSignature{
   309  				Content: (*strfmt.Base64)(&sig),
   310  				Format:  swag.String(models.RekordV001SchemaSignatureFormatPgp),
   311  				PublicKey: &models.RekordV001SchemaSignaturePublicKey{
   312  					Content: (*strfmt.Base64)(&pubKeyBytes),
   313  				},
   314  			},
   315  		},
   316  	}
   317  
   318  	returnVal := models.Rekord{
   319  		APIVersion: swag.String(re.APIVersion()),
   320  		Spec:       re.RekordObj,
   321  	}
   322  	entryBytes, err := json.Marshal(returnVal)
   323  	if err != nil {
   324  		t.Fatal(err)
   325  	}
   326  	if err := ioutil.WriteFile(entryPath, entryBytes, 0644); err != nil {
   327  		t.Fatal(err)
   328  	}
   329  
   330  	// Start pubsub client to capture notifications. Values match those in
   331  	// docker-compose.test.yml.
   332  	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
   333  	defer cancel()
   334  	psc, err := pubsub.NewClient(ctx, "test-project")
   335  	if err != nil {
   336  		t.Fatalf("Create pubsub client: %v", err)
   337  	}
   338  	topic, err := psc.CreateTopic(ctx, "new-entry")
   339  	if err != nil {
   340  		// Assume error is AlreadyExists if one occurrs unless it is context timeout.
   341  		// If the error was not AlreadyExists, it will be caught in later error
   342  		// checks in this test.
   343  		if errors.Is(err, os.ErrDeadlineExceeded) {
   344  			t.Fatalf("Create pubsub topic: %v", err)
   345  		}
   346  		topic = psc.Topic("new-entry")
   347  	}
   348  	filters := []string{
   349  		`attributes:rekor_entry_kind`,                          // Ignore any messages that do not have this attribute
   350  		`attributes.rekor_signing_subjects = "test@rekor.dev"`, // This is the email in the hard-coded PGP test key
   351  		`attributes.datacontenttype = "application/json"`,      // Only fetch the JSON formatted events
   352  	}
   353  	cfg := pubsub.SubscriptionConfig{
   354  		Topic:  topic,
   355  		Filter: strings.Join(filters, " AND "),
   356  	}
   357  	sub, err := psc.CreateSubscription(ctx, "new-entry-sub", cfg)
   358  	if err != nil {
   359  		if errors.Is(err, os.ErrDeadlineExceeded) {
   360  			t.Fatalf("Create pubsub subscription: %v", err)
   361  		}
   362  		sub = psc.Subscription("new-entry-sub")
   363  	}
   364  	ch := make(chan []byte, 1)
   365  	go func() {
   366  		if err := sub.Receive(ctx, func(_ context.Context, m *pubsub.Message) {
   367  			ch <- m.Data
   368  		}); err != nil {
   369  			t.Errorf("Receive pubusub msg: %v", err)
   370  		}
   371  	}()
   372  
   373  	// Now upload to rekor!
   374  	out := runCli(t, "upload", "--entry", entryPath)
   375  	outputContains(t, out, "Created entry at")
   376  
   377  	// Await pubsub
   378  	select {
   379  	case msg := <-ch:
   380  		t.Logf("Got pubsub message!\n%s", string(msg))
   381  	case <-ctx.Done():
   382  		t.Errorf("Did not receive pubsub message: %v", ctx.Err())
   383  	}
   384  }
   385  
   386  // Regression test for https://github.com/sigstore/rekor/pull/956
   387  // Requesting an inclusion proof concurrently with an entry write triggers
   388  // a race where the inclusion proof returned does not verify because the
   389  // tree head changes.
   390  func TestInclusionProofRace(t *testing.T) {
   391  	// Create a random artifact and sign it.
   392  	artifactPath := filepath.Join(t.TempDir(), "artifact")
   393  	sigPath := filepath.Join(t.TempDir(), "signature.asc")
   394  
   395  	sigx509.CreatedX509SignedArtifact(t, artifactPath, sigPath)
   396  	dataBytes, _ := ioutil.ReadFile(artifactPath)
   397  	h := sha256.Sum256(dataBytes)
   398  	dataSHA := hex.EncodeToString(h[:])
   399  
   400  	// Write the public key to a file
   401  	pubPath := filepath.Join(t.TempDir(), "pubKey.asc")
   402  	if err := ioutil.WriteFile(pubPath, []byte(sigx509.RSACert), 0644); err != nil {
   403  		t.Fatal(err)
   404  	}
   405  
   406  	// Upload an entry
   407  	runCli(t, "upload", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
   408  
   409  	// Constantly uploads new signatures on an entry.
   410  	uploadRoutine := func(pubPath string) error {
   411  		// Create a random artifact and sign it.
   412  		artifactPath := filepath.Join(t.TempDir(), "artifact")
   413  		sigPath := filepath.Join(t.TempDir(), "signature.asc")
   414  
   415  		sigx509.CreatedX509SignedArtifact(t, artifactPath, sigPath)
   416  		dataBytes, _ := ioutil.ReadFile(artifactPath)
   417  		h := sha256.Sum256(dataBytes)
   418  		dataSHA := hex.EncodeToString(h[:])
   419  
   420  		// Upload an entry
   421  		out := runCli(t, "upload", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
   422  		outputContains(t, out, "Created entry at")
   423  
   424  		return nil
   425  	}
   426  
   427  	// Attempts to verify the original entry.
   428  	verifyRoutine := func(dataSHA, sigPath, pubPath string) error {
   429  		out := runCli(t, "verify", "--type=hashedrekord", "--pki-format=x509", "--artifact-hash", dataSHA, "--signature", sigPath, "--public-key", pubPath)
   430  
   431  		if strings.Contains(out, "calculated root") || strings.Contains(out, "wrong") {
   432  			return errors.New(out)
   433  		}
   434  
   435  		return nil
   436  	}
   437  
   438  	var g errgroup.Group
   439  	for i := 0; i < 50; i++ {
   440  		g.Go(func() error { return uploadRoutine(pubPath) })
   441  		g.Go(func() error { return verifyRoutine(dataSHA, sigPath, pubPath) })
   442  	}
   443  
   444  	if err := g.Wait(); err != nil {
   445  		t.Fatal(err)
   446  	}
   447  }
   448  
   449  // TestIssue1308 should be run once before any other tests (against an empty log)
   450  func TestIssue1308(t *testing.T) {
   451  	// we run this to validate issue 1308 which needs to be tested against an empty log
   452  	if getTotalTreeSize(t) == 0 {
   453  		TestSearchQueryNonExistentEntry(t)
   454  	} else {
   455  		t.Skip("skipping because log is not empty")
   456  	}
   457  }
   458  
   459  func TestSearchQueryNonExistentEntry(t *testing.T) {
   460  	// Nonexistent but well-formed entry results in 200 with empty array as body
   461  	wd, err := os.Getwd()
   462  	if err != nil {
   463  		t.Fatal(err)
   464  	}
   465  	b, err := ioutil.ReadFile(filepath.Join(wd, "canonical_rekor.json"))
   466  	if err != nil {
   467  		t.Fatal(err)
   468  	}
   469  	body := fmt.Sprintf("{\"entries\":[%s]}", b)
   470  	resp, err := http.Post(fmt.Sprintf("%s/api/v1/log/entries/retrieve", rekorServer()),
   471  		"application/json",
   472  		bytes.NewBuffer([]byte(body)))
   473  	if err != nil {
   474  		t.Fatal(err)
   475  	}
   476  	c, _ := ioutil.ReadAll(resp.Body)
   477  	if resp.StatusCode != 200 {
   478  		t.Fatalf("expected status 200, got %d instead: %v", resp.StatusCode, string(c))
   479  	}
   480  	if strings.TrimSpace(string(c)) != "[]" {
   481  		t.Fatalf("expected empty JSON array as response, got %s instead", string(c))
   482  	}
   483  }
   484  
   485  func getTreeID(t *testing.T) int64 {
   486  	out := runCli(t, "loginfo")
   487  	tidStr := strings.TrimSpace(strings.Split(out, "TreeID: ")[1])
   488  	tid, err := strconv.ParseInt(tidStr, 10, 64)
   489  	if err != nil {
   490  		t.Errorf(err.Error())
   491  	}
   492  	t.Log("Tree ID:", tid)
   493  	return tid
   494  }
   495  
   496  func getTotalTreeSize(t *testing.T) int64 {
   497  	out := runCli(t, "loginfo")
   498  	sizeStr := strings.Fields(strings.Split(out, "Total Tree Size: ")[1])[0]
   499  	size, err := strconv.ParseInt(sizeStr, 10, 64)
   500  	if err != nil {
   501  		t.Errorf(err.Error())
   502  	}
   503  	t.Log("Total Tree Size:", size)
   504  	return size
   505  }
   506  
   507  // This test confirms that we validate tree ID when using the /api/v1/log/entries/retrieve endpoint
   508  // https://github.com/sigstore/rekor/issues/1014
   509  func TestSearchValidateTreeID(t *testing.T) {
   510  	// Create something and add it to the log
   511  	artifactPath := filepath.Join(t.TempDir(), "artifact")
   512  	sigPath := filepath.Join(t.TempDir(), "signature.asc")
   513  
   514  	createdPGPSignedArtifact(t, artifactPath, sigPath)
   515  
   516  	// Write the public key to a file
   517  	pubPath := filepath.Join(t.TempDir(), "pubKey.asc")
   518  	if err := ioutil.WriteFile(pubPath, []byte(publicKey), 0644); err != nil {
   519  		t.Fatal(err)
   520  	}
   521  	out := runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath, "--public-key", pubPath)
   522  	outputContains(t, out, "Created entry at")
   523  
   524  	uuid, err := sharding.GetUUIDFromIDString(getUUIDFromUploadOutput(t, out))
   525  	if err != nil {
   526  		t.Error(err)
   527  	}
   528  	// Make sure we can get by Entry ID
   529  	tid := getTreeID(t)
   530  	entryID, err := sharding.CreateEntryIDFromParts(fmt.Sprintf("%x", tid), uuid)
   531  	if err != nil {
   532  		t.Fatal(err)
   533  	}
   534  	body := "{\"entryUUIDs\":[\"%s\"]}"
   535  	resp, err := http.Post(fmt.Sprintf("%s/api/v1/log/entries/retrieve", rekorServer()), "application/json", bytes.NewBuffer([]byte(fmt.Sprintf(body, entryID.ReturnEntryIDString()))))
   536  	if err != nil {
   537  		t.Fatal(err)
   538  	}
   539  	if resp.StatusCode != 200 {
   540  		t.Fatalf("expected 200 status code but got %d", resp.StatusCode)
   541  	}
   542  
   543  	// Make sure we fail with a random tree ID
   544  	fakeTID := tid + 1
   545  	entryID, err = sharding.CreateEntryIDFromParts(fmt.Sprintf("%x", fakeTID), uuid)
   546  	if err != nil {
   547  		t.Fatal(err)
   548  	}
   549  
   550  	resp, err = http.Post(fmt.Sprintf("%s/api/v1/log/entries/retrieve", rekorServer()), "application/json", bytes.NewBuffer([]byte(fmt.Sprintf(body, entryID.ReturnEntryIDString()))))
   551  	if err != nil {
   552  		t.Fatal(err)
   553  	}
   554  	// Not Found because currently we don't detect that an unused random tree ID is invalid.
   555  	c, _ := ioutil.ReadAll(resp.Body)
   556  	if resp.StatusCode != 200 {
   557  		t.Fatalf("expected status 200, got %d instead", resp.StatusCode)
   558  	}
   559  	if strings.TrimSpace(string(c)) != "[]" {
   560  		t.Fatalf("expected empty JSON array as response, got %s instead", string(c))
   561  	}
   562  }
   563  
   564  // TestSearchLogQuerySingleShard provides coverage testing on the searchLogQuery endpoint within a single shard
   565  func TestSearchLogQuerySingleShard(t *testing.T) {
   566  
   567  	// Write the shared public key to a file
   568  	pubPath := filepath.Join(t.TempDir(), "logQuery_pubKey.asc")
   569  	pubKeyBytes := []byte(publicKey)
   570  	if err := ioutil.WriteFile(pubPath, pubKeyBytes, 0644); err != nil {
   571  		t.Fatal(err)
   572  	}
   573  
   574  	// Create two valid log entries to use for the test cases
   575  	firstArtifactPath := filepath.Join(t.TempDir(), "artifact1")
   576  	firstSigPath := filepath.Join(t.TempDir(), "signature1.asc")
   577  	createdPGPSignedArtifact(t, firstArtifactPath, firstSigPath)
   578  	firstArtifactBytes, _ := ioutil.ReadFile(firstArtifactPath)
   579  	firstSigBytes, _ := ioutil.ReadFile(firstSigPath)
   580  
   581  	firstRekord := rekord.V001Entry{
   582  		RekordObj: models.RekordV001Schema{
   583  			Data: &models.RekordV001SchemaData{
   584  				Content: strfmt.Base64(firstArtifactBytes),
   585  			},
   586  			Signature: &models.RekordV001SchemaSignature{
   587  				Content: (*strfmt.Base64)(&firstSigBytes),
   588  				Format:  swag.String(models.RekordV001SchemaSignatureFormatPgp),
   589  				PublicKey: &models.RekordV001SchemaSignaturePublicKey{
   590  					Content: (*strfmt.Base64)(&pubKeyBytes),
   591  				},
   592  			},
   593  		},
   594  	}
   595  	firstEntry := &models.Rekord{
   596  		APIVersion: swag.String(firstRekord.APIVersion()),
   597  		Spec:       firstRekord.RekordObj,
   598  	}
   599  
   600  	secondArtifactPath := filepath.Join(t.TempDir(), "artifact2")
   601  	secondSigPath := filepath.Join(t.TempDir(), "signature2.asc")
   602  	createdPGPSignedArtifact(t, secondArtifactPath, secondSigPath)
   603  	secondArtifactBytes, _ := ioutil.ReadFile(secondArtifactPath)
   604  	secondSigBytes, _ := ioutil.ReadFile(secondSigPath)
   605  
   606  	secondRekord := rekord.V001Entry{
   607  		RekordObj: models.RekordV001Schema{
   608  			Data: &models.RekordV001SchemaData{
   609  				Content: strfmt.Base64(secondArtifactBytes),
   610  			},
   611  			Signature: &models.RekordV001SchemaSignature{
   612  				Content: (*strfmt.Base64)(&secondSigBytes),
   613  				Format:  swag.String(models.RekordV001SchemaSignatureFormatPgp),
   614  				PublicKey: &models.RekordV001SchemaSignaturePublicKey{
   615  					Content: (*strfmt.Base64)(&pubKeyBytes),
   616  				},
   617  			},
   618  		},
   619  	}
   620  	secondEntry := &models.Rekord{
   621  		APIVersion: swag.String(secondRekord.APIVersion()),
   622  		Spec:       secondRekord.RekordObj,
   623  	}
   624  
   625  	// Now upload them to rekor!
   626  	firstOut := runCli(t, "upload", "--artifact", firstArtifactPath, "--signature", firstSigPath, "--public-key", pubPath)
   627  	secondOut := runCli(t, "upload", "--artifact", secondArtifactPath, "--signature", secondSigPath, "--public-key", pubPath)
   628  
   629  	firstEntryID := getUUIDFromUploadOutput(t, firstOut)
   630  	firstUUID, _ := sharding.GetUUIDFromIDString(firstEntryID)
   631  	firstIndex := int64(getLogIndexFromUploadOutput(t, firstOut))
   632  	secondEntryID := getUUIDFromUploadOutput(t, secondOut)
   633  	secondUUID, _ := sharding.GetUUIDFromIDString(secondEntryID)
   634  	secondIndex := int64(getLogIndexFromUploadOutput(t, secondOut))
   635  
   636  	// this is invalid because treeID is > int64
   637  	invalidEntryID := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeeefff"
   638  	invalidIndex := int64(-1)
   639  	invalidEntry := &models.Rekord{
   640  		APIVersion: swag.String(secondRekord.APIVersion()),
   641  	}
   642  
   643  	nonexistentArtifactPath := filepath.Join(t.TempDir(), "artifact3")
   644  	nonexistentSigPath := filepath.Join(t.TempDir(), "signature3.asc")
   645  	createdPGPSignedArtifact(t, nonexistentArtifactPath, nonexistentSigPath)
   646  	nonexistentArtifactBytes, _ := ioutil.ReadFile(nonexistentArtifactPath)
   647  	nonexistentSigBytes, _ := ioutil.ReadFile(nonexistentSigPath)
   648  
   649  	nonexistentEntryID := "0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeeefff"
   650  	nonexistentUUID := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeeefff"
   651  	nonexistentIndex := int64(999999999) // assuming we don't put that many entries in the log
   652  	nonexistentRekord := rekord.V001Entry{
   653  		RekordObj: models.RekordV001Schema{
   654  			Data: &models.RekordV001SchemaData{
   655  				Content: strfmt.Base64(nonexistentArtifactBytes),
   656  			},
   657  			Signature: &models.RekordV001SchemaSignature{
   658  				Content: (*strfmt.Base64)(&nonexistentSigBytes),
   659  				Format:  swag.String(models.RekordV001SchemaSignatureFormatPgp),
   660  				PublicKey: &models.RekordV001SchemaSignaturePublicKey{
   661  					Content: (*strfmt.Base64)(&pubKeyBytes),
   662  				},
   663  			},
   664  		},
   665  	}
   666  	nonexistentEntry := &models.Rekord{
   667  		APIVersion: swag.String("0.0.1"),
   668  		Spec:       nonexistentRekord.RekordObj,
   669  	}
   670  
   671  	type testCase struct {
   672  		name                      string
   673  		expectSuccess             bool
   674  		expectedErrorResponseCode int64
   675  		expectedEntryIDs          []string
   676  		entryUUIDs                []string
   677  		logIndexes                []*int64
   678  		entries                   []models.ProposedEntry
   679  	}
   680  
   681  	testCases := []testCase{
   682  		{
   683  			name:             "empty entryUUIDs",
   684  			expectSuccess:    true,
   685  			expectedEntryIDs: []string{},
   686  			entryUUIDs:       []string{},
   687  		},
   688  		{
   689  			name:             "first in log (using entryUUIDs)",
   690  			expectSuccess:    true,
   691  			expectedEntryIDs: []string{firstEntryID},
   692  			entryUUIDs:       []string{firstEntryID},
   693  		},
   694  		{
   695  			name:             "first in log (using UUID in entryUUIDs)",
   696  			expectSuccess:    true,
   697  			expectedEntryIDs: []string{firstEntryID},
   698  			entryUUIDs:       []string{firstUUID},
   699  		},
   700  		{
   701  			name:             "second in log (using entryUUIDs)",
   702  			expectSuccess:    true,
   703  			expectedEntryIDs: []string{secondEntryID},
   704  			entryUUIDs:       []string{secondEntryID},
   705  		},
   706  		{
   707  			name:                      "invalid entryID (using entryUUIDs)",
   708  			expectSuccess:             false,
   709  			expectedErrorResponseCode: http.StatusBadRequest,
   710  			entryUUIDs:                []string{invalidEntryID},
   711  		},
   712  		{
   713  			name:             "valid entryID not in log (using entryUUIDs)",
   714  			expectSuccess:    true,
   715  			expectedEntryIDs: []string{},
   716  			entryUUIDs:       []string{nonexistentEntryID},
   717  		},
   718  		{
   719  			name:             "valid UUID not in log (using entryUUIDs)",
   720  			expectSuccess:    true,
   721  			expectedEntryIDs: []string{},
   722  			entryUUIDs:       []string{nonexistentUUID},
   723  		},
   724  		{
   725  			name:             "both valid entries in log (using entryUUIDs)",
   726  			expectSuccess:    true,
   727  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   728  			entryUUIDs:       []string{firstEntryID, secondEntryID},
   729  		},
   730  		{
   731  			name:             "both valid entries in log (one with UUID, other with entryID) (using entryUUIDs)",
   732  			expectSuccess:    true,
   733  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   734  			entryUUIDs:       []string{firstEntryID, secondUUID},
   735  		},
   736  		{
   737  			name:                      "one valid entry in log, one malformed (using entryUUIDs)",
   738  			expectSuccess:             false,
   739  			expectedErrorResponseCode: http.StatusBadRequest,
   740  			entryUUIDs:                []string{firstEntryID, invalidEntryID},
   741  		},
   742  		{
   743  			name:             "one existing, one valid entryID but not in log (using entryUUIDs)",
   744  			expectSuccess:    true,
   745  			expectedEntryIDs: []string{firstEntryID},
   746  			entryUUIDs:       []string{firstEntryID, nonexistentEntryID},
   747  		},
   748  		{
   749  			name:             "two existing, one valid entryID but not in log (using entryUUIDs)",
   750  			expectSuccess:    true,
   751  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   752  			entryUUIDs:       []string{firstEntryID, secondEntryID, nonexistentEntryID},
   753  		},
   754  		{
   755  			name:             "two existing, one valid entryID but not in log (different ordering 1) (using entryUUIDs)",
   756  			expectSuccess:    true,
   757  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   758  			entryUUIDs:       []string{firstEntryID, nonexistentEntryID, secondEntryID},
   759  		},
   760  		{
   761  			name:             "two existing, one valid entryID but not in log (different ordering 2) (using entryUUIDs)",
   762  			expectSuccess:    true,
   763  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   764  			entryUUIDs:       []string{nonexistentEntryID, firstEntryID, secondEntryID},
   765  		},
   766  		{
   767  			name:             "two existing, one valid entryID but not in log (different ordering 3) (using entryUUIDs)",
   768  			expectSuccess:    true,
   769  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   770  			entryUUIDs:       []string{nonexistentUUID, firstEntryID, secondEntryID},
   771  		},
   772  		{
   773  			name:             "two existing, one valid entryID but not in log (different ordering 4) (using entryUUIDs)",
   774  			expectSuccess:    true,
   775  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   776  			entryUUIDs:       []string{nonexistentEntryID, firstUUID, secondEntryID},
   777  		},
   778  		{
   779  			name:             "two existing, one valid entryID but not in log (different ordering 5) (using entryUUIDs)",
   780  			expectSuccess:    true,
   781  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   782  			entryUUIDs:       []string{nonexistentEntryID, firstEntryID, secondUUID},
   783  		},
   784  		{
   785  			name:             "two existing, one valid entryID but not in log (different ordering 6) (using entryUUIDs)",
   786  			expectSuccess:    true,
   787  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   788  			entryUUIDs:       []string{nonexistentUUID, firstEntryID, secondUUID},
   789  		},
   790  		{
   791  			name:             "two existing, one valid entryID but not in log (different ordering 7) (using entryUUIDs)",
   792  			expectSuccess:    true,
   793  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   794  			entryUUIDs:       []string{nonexistentEntryID, firstUUID, secondUUID},
   795  		},
   796  		{
   797  			name:                      "request more than 10 entries (using entryUUIDs)",
   798  			expectSuccess:             false,
   799  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   800  			entryUUIDs:                []string{firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID, firstEntryID},
   801  		},
   802  		{
   803  			name:             "empty logIndexes",
   804  			expectSuccess:    true,
   805  			expectedEntryIDs: []string{},
   806  			logIndexes:       []*int64{},
   807  		},
   808  		{
   809  			name:             "first in log (using logIndexes)",
   810  			expectSuccess:    true,
   811  			expectedEntryIDs: []string{firstEntryID},
   812  			logIndexes:       []*int64{&firstIndex},
   813  		},
   814  		{
   815  			name:             "second in log (using logIndexes)",
   816  			expectSuccess:    true,
   817  			expectedEntryIDs: []string{secondEntryID},
   818  			logIndexes:       []*int64{&secondIndex},
   819  		},
   820  		{
   821  			name:                      "invalid logIndex (using logIndexes)",
   822  			expectSuccess:             false,
   823  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   824  			logIndexes:                []*int64{&invalidIndex},
   825  		},
   826  		{
   827  			name:             "valid index not in log (using logIndexes)",
   828  			expectSuccess:    true,
   829  			expectedEntryIDs: []string{},
   830  			logIndexes:       []*int64{&nonexistentIndex},
   831  		},
   832  		{
   833  			name:             "both valid entries in log (using logIndexes)",
   834  			expectSuccess:    true,
   835  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   836  			logIndexes:       []*int64{&firstIndex, &secondIndex},
   837  		},
   838  		{
   839  			name:                      "one valid entry in log, one malformed (using logIndexes)",
   840  			expectSuccess:             false,
   841  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   842  			logIndexes:                []*int64{&firstIndex, &invalidIndex},
   843  		},
   844  		{
   845  			name:             "one existing, one valid Index but not in log (using logIndexes)",
   846  			expectSuccess:    true,
   847  			expectedEntryIDs: []string{firstEntryID},
   848  			logIndexes:       []*int64{&firstIndex, &nonexistentIndex},
   849  		},
   850  		{
   851  			name:             "two existing, one valid Index but not in log (using logIndexes)",
   852  			expectSuccess:    true,
   853  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   854  			logIndexes:       []*int64{&firstIndex, &secondIndex, &nonexistentIndex},
   855  		},
   856  		{
   857  			name:             "two existing, one valid Index but not in log (different ordering 1) (using logIndexes)",
   858  			expectSuccess:    true,
   859  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   860  			logIndexes:       []*int64{&firstIndex, &nonexistentIndex, &secondIndex},
   861  		},
   862  		{
   863  			name:             "two existing, one valid Index but not in log (different ordering 2) (using logIndexes)",
   864  			expectSuccess:    true,
   865  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   866  			logIndexes:       []*int64{&nonexistentIndex, &firstIndex, &secondIndex},
   867  		},
   868  		{
   869  			name:                      "request more than 10 entries (using logIndexes)",
   870  			expectSuccess:             false,
   871  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   872  			logIndexes:                []*int64{&firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex, &firstIndex},
   873  		},
   874  		{
   875  			name:             "empty entries",
   876  			expectSuccess:    true,
   877  			expectedEntryIDs: []string{},
   878  			entries:          []models.ProposedEntry{},
   879  		},
   880  		{
   881  			name:             "first in log (using entries)",
   882  			expectSuccess:    true,
   883  			expectedEntryIDs: []string{firstEntryID},
   884  			entries:          []models.ProposedEntry{firstEntry},
   885  		},
   886  		{
   887  			name:             "second in log (using entries)",
   888  			expectSuccess:    true,
   889  			expectedEntryIDs: []string{secondEntryID},
   890  			entries:          []models.ProposedEntry{secondEntry},
   891  		},
   892  		{
   893  			name:                      "invalid entry (using entries)",
   894  			expectSuccess:             false,
   895  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   896  			entries:                   []models.ProposedEntry{invalidEntry},
   897  		},
   898  		{
   899  			name:             "valid entry not in log (using entries)",
   900  			expectSuccess:    true,
   901  			expectedEntryIDs: []string{},
   902  			entries:          []models.ProposedEntry{nonexistentEntry},
   903  		},
   904  		{
   905  			name:             "both valid entries in log (using entries)",
   906  			expectSuccess:    true,
   907  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   908  			entries:          []models.ProposedEntry{firstEntry, secondEntry},
   909  		},
   910  		{
   911  			name:                      "one valid entry in log, one malformed (using entries)",
   912  			expectSuccess:             false,
   913  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   914  			entries:                   []models.ProposedEntry{firstEntry, invalidEntry},
   915  		},
   916  		{
   917  			name:             "one existing, one valid Index but not in log (using entries)",
   918  			expectSuccess:    true,
   919  			expectedEntryIDs: []string{firstEntryID},
   920  			entries:          []models.ProposedEntry{firstEntry, nonexistentEntry},
   921  		},
   922  		{
   923  			name:             "two existing, one valid Index but not in log (using entries)",
   924  			expectSuccess:    true,
   925  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   926  			entries:          []models.ProposedEntry{firstEntry, secondEntry, nonexistentEntry},
   927  		},
   928  		{
   929  			name:             "two existing, one valid Index but not in log (different ordering 1) (using entries)",
   930  			expectSuccess:    true,
   931  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   932  			entries:          []models.ProposedEntry{firstEntry, nonexistentEntry, secondEntry},
   933  		},
   934  		{
   935  			name:             "two existing, one valid Index but not in log (different ordering 2) (using entries)",
   936  			expectSuccess:    true,
   937  			expectedEntryIDs: []string{firstEntryID, secondEntryID},
   938  			entries:          []models.ProposedEntry{nonexistentEntry, firstEntry, secondEntry},
   939  		},
   940  		{
   941  			name:                      "request more than 10 entries (using entries)",
   942  			expectSuccess:             false,
   943  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   944  			entries:                   []models.ProposedEntry{firstEntry, firstEntry, firstEntry, firstEntry, firstEntry, firstEntry, firstEntry, firstEntry, firstEntry, firstEntry, firstEntry},
   945  		},
   946  		{
   947  			name:                      "request more than 10 entries (using mixture)",
   948  			expectSuccess:             false,
   949  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   950  			entryUUIDs:                []string{firstEntryID, firstEntryID, firstEntryID, firstEntryID},
   951  			logIndexes:                []*int64{&firstIndex, &firstIndex, &firstIndex},
   952  			entries:                   []models.ProposedEntry{firstEntry, firstEntry, firstEntry, firstEntry},
   953  		},
   954  		{
   955  			name:                      "request valid and invalid (using mixture)",
   956  			expectSuccess:             false,
   957  			expectedErrorResponseCode: http.StatusUnprocessableEntity,
   958  			entryUUIDs:                []string{firstEntryID, firstEntryID, firstEntryID, firstEntryID},
   959  			logIndexes:                []*int64{&invalidIndex, &invalidIndex, &invalidIndex},
   960  			entries:                   []models.ProposedEntry{firstEntry, firstEntry, firstEntry},
   961  		},
   962  		{
   963  			name:             "request valid and nonexistent (using mixture)",
   964  			expectSuccess:    true,
   965  			expectedEntryIDs: []string{firstEntryID, secondEntryID, firstEntryID, secondEntryID, firstEntryID, secondEntryID},
   966  			entryUUIDs:       []string{firstEntryID, secondEntryID, nonexistentEntryID},
   967  			logIndexes:       []*int64{&firstIndex, &secondIndex, &nonexistentIndex},
   968  			entries:          []models.ProposedEntry{firstEntry, secondEntry, nonexistentEntry},
   969  		},
   970  	}
   971  
   972  	for _, test := range testCases {
   973  		rekorClient, err := client.GetRekorClient(rekorServer(), client.WithRetryCount(0))
   974  		if err != nil {
   975  			t.Fatal(err)
   976  		}
   977  		t.Run(test.name, func(t *testing.T) {
   978  			params := entries.NewSearchLogQueryParams()
   979  			entry := &models.SearchLogQuery{}
   980  			if len(test.entryUUIDs) > 0 {
   981  				t.Log("trying with entryUUIDs: ", test.entryUUIDs)
   982  				entry.EntryUUIDs = test.entryUUIDs
   983  			}
   984  			if len(test.logIndexes) > 0 {
   985  				entry.LogIndexes = test.logIndexes
   986  			}
   987  			if len(test.entries) > 0 {
   988  				entry.SetEntries(test.entries)
   989  			}
   990  			params.SetEntry(entry)
   991  
   992  			resp, err := rekorClient.Entries.SearchLogQuery(params)
   993  			if err != nil {
   994  				if !test.expectSuccess {
   995  					if _, ok := err.(*entries.SearchLogQueryBadRequest); ok {
   996  						if test.expectedErrorResponseCode != http.StatusBadRequest {
   997  							t.Fatalf("unexpected error code received: expected %d, got %d: %v", test.expectedErrorResponseCode, http.StatusBadRequest, err)
   998  						}
   999  					} else if _, ok := err.(*entries.SearchLogQueryUnprocessableEntity); ok {
  1000  						if test.expectedErrorResponseCode != http.StatusUnprocessableEntity {
  1001  							t.Fatalf("unexpected error code received: expected %d, got %d: %v", test.expectedErrorResponseCode, http.StatusUnprocessableEntity, err)
  1002  						}
  1003  					} else if e, ok := err.(*entries.SearchLogQueryDefault); ok {
  1004  						t.Fatalf("unexpected error: %v", e)
  1005  					}
  1006  				} else {
  1007  					t.Fatalf("unexpected error: %v", err)
  1008  				}
  1009  			} else {
  1010  				if len(resp.Payload) != len(test.expectedEntryIDs) {
  1011  					t.Fatalf("unexpected number of responses received: expected %d, got %d", len(test.expectedEntryIDs), len(resp.Payload))
  1012  				}
  1013  				// walk responses, build up list of returned entry IDs
  1014  				returnedEntryIDs := []string{}
  1015  				for _, entry := range resp.Payload {
  1016  					// do this for dynamic keyed entries
  1017  					for entryID := range entry {
  1018  						t.Log("received entry: ", entryID)
  1019  						returnedEntryIDs = append(returnedEntryIDs, entryID)
  1020  					}
  1021  				}
  1022  				// we have the expected number of responses, let's ensure they're the ones we expected
  1023  				if out := cmp.Diff(returnedEntryIDs, test.expectedEntryIDs, cmpopts.SortSlices(func(a, b string) bool { return a < b })); out != "" {
  1024  					t.Fatalf("unexpected responses: %v", out)
  1025  				}
  1026  			}
  1027  		})
  1028  	}
  1029  }
  1030  

View as plain text