From 7ecbf61ebf89d478afac1bd5216e24ca8a116a55 Mon Sep 17 00:00:00 2001 From: linus-sun Date: Wed, 16 Oct 2024 21:24:08 +0000 Subject: [PATCH] add identity search workflow Signed-off-by: linus-sun --- .github/workflows/main.yml | 2 + cmd/monitor/config.go | 37 +++ cmd/monitor/main.go | 143 +++++++++++ pkg/notifications/email.go | 12 +- pkg/notifications/github_issues.go | 10 +- pkg/notifications/mailgun.go | 8 +- pkg/notifications/sendgrid.go | 10 +- pkg/rekor/identity.go | 73 ++++-- pkg/rekor/verifier.go | 14 +- .../identity_workflow_e2e_test.go | 224 ++++++++++++++++++ .../identity_workflow_e2e_test.sh | 69 ++++++ 11 files changed, 562 insertions(+), 40 deletions(-) create mode 100644 cmd/monitor/config.go create mode 100644 cmd/monitor/main.go create mode 100644 pkg/test/identity_workflow/identity_workflow_e2e_test.go create mode 100755 pkg/test/identity_workflow/identity_workflow_e2e_test.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 204fa32..30766df 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,4 +90,6 @@ jobs: check-latest: true - name: run e2e test run: ./pkg/test/e2e/e2e_test.sh + - name: run identity monitor test + run: ./pkg/test/identity_workflow/identity_workflow_e2e_test.sh diff --git a/cmd/monitor/config.go b/cmd/monitor/config.go new file mode 100644 index 0000000..610ca53 --- /dev/null +++ b/cmd/monitor/config.go @@ -0,0 +1,37 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "time" + + "github.com/sigstore/rekor-monitor/pkg/identity" + "github.com/sigstore/rekor-monitor/pkg/notifications" +) + +type IdentityMonitorConfiguration struct { + StartIndex *int `yaml:"startIndex"` + EndIndex *int `yaml:"endIndex"` + MonitoredValues identity.MonitoredValues `yaml:"monitoredValues"` + ServerURL string `yaml:"serverURL"` + OutputIdentitiesFile string `yaml:"outputIdentities"` + LogInfoFile string `yaml:"logInfoFile"` + GitHubIssue notifications.GitHubIssueInput `yaml:"githubIssue"` + EmailNotificationSMTP notifications.EmailNotificationInput `yaml:"emailNotificationSMTP"` + EmailNotificationMailgun notifications.MailgunNotificationInput `yaml:"emailNotificationMailgun"` + EmailNotificationSendGrid notifications.SendGridNotificationInput `yaml:"emailNotificationSendGrid"` + Interval *time.Duration `yaml:"interval"` +} diff --git a/cmd/monitor/main.go b/cmd/monitor/main.go new file mode 100644 index 0000000..d53a6b5 --- /dev/null +++ b/cmd/monitor/main.go @@ -0,0 +1,143 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "runtime" + "strings" + "time" + + "github.com/sigstore/rekor-monitor/pkg/rekor" + "github.com/sigstore/rekor-monitor/pkg/util/file" + "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/util" + "gopkg.in/yaml.v2" + "sigs.k8s.io/release-utils/version" +) + +// Default values for monitoring job parameters +const ( + publicRekorServerURL = "https://rekor.sigstore.dev" + logInfoFileName = "logInfo.txt" + outputIdentitiesFileName = "identities.txt" +) + +// This main function performs a periodic identity search. +// Upon starting, any existing latest snapshot data is loaded and the function runs +// indefinitely to perform identity search for every time interval that was specified. +func main() { + // Command-line flags that are parameters to the verifier job + configFilePath := flag.String("config-file", "", "path to yaml configuration file containing identity monitor settings") + once := flag.Bool("once", true, "whether to run the monitor on a repeated interval or once") + flag.Parse() + + if configFilePath == nil { + log.Fatalf("empty configuration file path") + } + + readConfig, err := os.ReadFile(*configFilePath) + if err != nil { + log.Fatalf("error reading from identity monitor configuration file: %v", err) + } + + configString := string(readConfig) + var config IdentityMonitorConfiguration + if err := yaml.Unmarshal([]byte(configString), &config); err != nil { + log.Fatalf("error parsing identities: %v", err) + } + + rekorClient, err := client.GetRekorClient(config.ServerURL, client.WithUserAgent(strings.TrimSpace(fmt.Sprintf("rekor-monitor/%s (%s; %s)", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH)))) + if err != nil { + log.Fatalf("getting Rekor client: %v", err) + } + + if config.StartIndex == nil || config.EndIndex == nil { + logInfo, err := rekor.GetLogInfo(context.Background(), rekorClient) + if err != nil { + log.Fatalf("error getting log info: %v", err) + } + + checkpoint, err := rekor.ReadLatestCheckpoint(logInfo) + if err != nil { + log.Fatalf("error reading checkpoint: %v", err) + } + + var prevCheckpoint *util.SignedCheckpoint + prevCheckpoint, err = file.ReadLatestCheckpoint(config.LogInfoFile) + if err != nil { + log.Fatalf("reading checkpoint log: %v", err) + } + + checkpointStartIndex, checkpointEndIndex := rekor.GetCheckpointIndices(logInfo, prevCheckpoint, checkpoint) + if config.StartIndex == nil { + config.StartIndex = &checkpointStartIndex + } + if config.EndIndex == nil { + config.EndIndex = &checkpointEndIndex + } + } + + if config.ServerURL == "" { + config.ServerURL = publicRekorServerURL + } + if config.LogInfoFile == "" { + config.LogInfoFile = logInfoFileName + } + if config.OutputIdentitiesFile == "" { + config.OutputIdentitiesFile = outputIdentitiesFileName + } + if config.Interval == nil { + defaultInterval := time.Hour + config.Interval = &defaultInterval + } + + ticker := time.NewTicker(*config.Interval) + defer ticker.Stop() + + // To get an immediate first tick + for ; ; <-ticker.C { + err = rekor.IdentitySearch(*config.StartIndex, *config.EndIndex, rekorClient, config.MonitoredValues, config.OutputIdentitiesFile) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to successfully complete identity search: %v", err) + return + } + + if *once { + return + } + + prevCheckpoint, checkpoint, err := rekor.GetPrevCurrentCheckpoints(rekorClient, config.LogInfoFile) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to fetch previous and current checkpoints: %v", err) + return + } + + logInfo, err := rekor.GetLogInfo(context.Background(), rekorClient) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to fetch log info: %v", err) + return + } + + checkpointStartIndex, checkpointEndIndex := rekor.GetCheckpointIndices(logInfo, prevCheckpoint, checkpoint) + config.StartIndex = &checkpointStartIndex + config.EndIndex = &checkpointEndIndex + } +} diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 344784a..f15a55d 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -24,12 +24,12 @@ import ( // EmailNotificationInput extends the NotificationPlatform interface to support // found identity notification by sending emails to a specified user. type EmailNotificationInput struct { - RecipientEmailAddress string - SenderEmailAddress string - SenderSMTPUsername string - SenderSMTPPassword string - SMTPHostURL string - SMTPCustomOptions []mail.Option + RecipientEmailAddress string `yaml:"recipientEmailAddress"` + SenderEmailAddress string `yaml:"senderEmailAddress"` + SenderSMTPUsername string `yaml:"senderSMTPUsername"` + SenderSMTPPassword string `yaml:"senderSMTPPassword"` + SMTPHostURL string `yaml:"SMTPHostURL"` + SMTPCustomOptions []mail.Option `yaml:"SMTPCustomOptions"` } func GenerateEmailBody(monitoredIdentities []identity.MonitoredIdentity) (string, error) { diff --git a/pkg/notifications/github_issues.go b/pkg/notifications/github_issues.go index 90023d6..703b761 100644 --- a/pkg/notifications/github_issues.go +++ b/pkg/notifications/github_issues.go @@ -30,15 +30,15 @@ var ( // GitHubIssueInput extends the NotificationPlatform interface to support found identity // notification via creating new GitHub issues in a given repo. type GitHubIssueInput struct { - AssigneeUsername string - RepositoryOwner string - RepositoryName string + AssigneeUsername string `yaml:"assigneeUsername"` + RepositoryOwner string `yaml:"repositoryOwner"` + RepositoryName string `yaml:"repositoryName"` // The PAT or other access token to authenticate creating an issue. // The authentication token requires repo write and push access. - AuthenticationToken string + AuthenticationToken string `yaml:"authenticationToken"` // For users who want to pass in a custom client. // If nil, a default client with the given authentication token will be instantiated. - GitHubClient *github.Client + GitHubClient *github.Client `yaml:"githubClient"` } func generateGitHubIssueBody(monitoredIdentities []identity.MonitoredIdentity) (string, error) { diff --git a/pkg/notifications/mailgun.go b/pkg/notifications/mailgun.go index 2eeebe7..7a238e8 100644 --- a/pkg/notifications/mailgun.go +++ b/pkg/notifications/mailgun.go @@ -24,10 +24,10 @@ import ( // MailgunNotificationInput extends the NotificationPlatform interface to support // found identity notification by sending emails to a specified user via Mailgun. type MailgunNotificationInput struct { - RecipientEmailAddress string - SenderEmailAddress string - MailgunAPIKey string - MailgunDomainName string + RecipientEmailAddress string `yaml:"recipientEmailAddress"` + SenderEmailAddress string `yaml:"senderEmailAddress"` + MailgunAPIKey string `yaml:"mailgunAPIKey"` + MailgunDomainName string `yaml:"mailgunDomainName"` } // Send takes in an MailgunNotificationInput and attempts to send the diff --git a/pkg/notifications/sendgrid.go b/pkg/notifications/sendgrid.go index 5b03e15..199728a 100644 --- a/pkg/notifications/sendgrid.go +++ b/pkg/notifications/sendgrid.go @@ -25,11 +25,11 @@ import ( // SendGrid extends the NotificationPlatform interface to support // found identity notification by sending emails to a specified user via SendGrid. type SendGridNotificationInput struct { - RecipientName string - RecipientEmailAddress string - SenderName string - SenderEmailAddress string - SendGridAPIKey string + RecipientName string `yaml:"recipientName"` + RecipientEmailAddress string `yaml:"recipientEmailAddress"` + SenderName string `yaml:"senderName"` + SenderEmailAddress string `yaml:"senderEmailAddress"` + SendGridAPIKey string `yaml:"sendGridAPIKey"` } // Send takes in an SendGridNotificationInput and attempts to send the diff --git a/pkg/rekor/identity.go b/pkg/rekor/identity.go index b418837..9a3ac19 100644 --- a/pkg/rekor/identity.go +++ b/pkg/rekor/identity.go @@ -341,8 +341,28 @@ func oidMatchesPolicy(cert *x509.Certificate, oid asn1.ObjectIdentifier, extensi return false, nil, "", nil } -// writeIdentitiesBetweenCheckpoints monitors for given identities between two checkpoints and writes any found identities to file. -func writeIdentitiesBetweenCheckpoints(logInfo *models.LogInfo, prevCheckpoint *util.SignedCheckpoint, checkpoint *util.SignedCheckpoint, monitoredValues identity.MonitoredValues, rekorClient *client.Rekor, outputIdentitiesFile string) error { +func GetPrevCurrentCheckpoints(client *client.Rekor, logInfoFile string) (*util.SignedCheckpoint, *util.SignedCheckpoint, error) { + logInfo, err := GetLogInfo(context.Background(), client) + if err != nil { + return nil, nil, fmt.Errorf("error getting log info: %v", err) + } + + checkpoint, err := ReadLatestCheckpoint(logInfo) + if err != nil { + return nil, nil, fmt.Errorf("error reading checkpoint: %v", err) + } + + var prevCheckpoint *util.SignedCheckpoint + prevCheckpoint, err = file.ReadLatestCheckpoint(logInfoFile) + if err != nil { + return nil, nil, fmt.Errorf("reading checkpoint log: %v", err) + } + + return prevCheckpoint, checkpoint, nil +} + +// GetCheckpointIndices fetches the start and end indexes between two checkpoints and returns them. +func GetCheckpointIndices(logInfo *models.LogInfo, prevCheckpoint *util.SignedCheckpoint, checkpoint *util.SignedCheckpoint) (int, int) { // Get log size of inactive shards totalSize := 0 for _, s := range logInfo.InactiveShards { @@ -351,26 +371,43 @@ func writeIdentitiesBetweenCheckpoints(logInfo *models.LogInfo, prevCheckpoint * startIndex := int(prevCheckpoint.Size) + totalSize - 1 //nolint: gosec // G115, log will never be large enough to overflow endIndex := int(checkpoint.Size) + totalSize - 1 //nolint: gosec // G115 - // Search for identities in the log range - if identity.MonitoredValuesExist(monitoredValues) { - entries, err := GetEntriesByIndexRange(context.Background(), rekorClient, startIndex, endIndex) - if err != nil { - return fmt.Errorf("error getting entries by index range: %v", err) - } - idEntries, err := MatchedIndices(entries, monitoredValues) - if err != nil { - return fmt.Errorf("error finding log indices: %v", err) - } + return startIndex, endIndex +} - if len(idEntries) > 0 { - for _, idEntry := range idEntries { - fmt.Fprintf(os.Stderr, "Found %s\n", idEntry.String()) +func IdentitySearch(startIndex int, endIndex int, rekorClient *client.Rekor, monitoredValues identity.MonitoredValues, outputIdentitiesFile string) error { - if err := file.WriteIdentity(outputIdentitiesFile, idEntry); err != nil { - return fmt.Errorf("failed to write entry: %v", err) - } + entries, err := GetEntriesByIndexRange(context.Background(), rekorClient, startIndex, endIndex) + if err != nil { + return fmt.Errorf("error getting entries by index range: %v", err) + } + idEntries, err := MatchedIndices(entries, monitoredValues) + if err != nil { + return fmt.Errorf("error finding log indices: %v", err) + } + + if len(idEntries) > 0 { + for _, idEntry := range idEntries { + fmt.Fprintf(os.Stderr, "Found %s\n", idEntry.String()) + + if err := file.WriteIdentity(outputIdentitiesFile, idEntry); err != nil { + return fmt.Errorf("failed to write entry: %v", err) } } } return nil } + +// writeIdentitiesBetweenCheckpoints monitors for given identities between two checkpoints and writes any found identities to file. +func writeIdentitiesBetweenCheckpoints(logInfo *models.LogInfo, prevCheckpoint *util.SignedCheckpoint, checkpoint *util.SignedCheckpoint, monitoredValues identity.MonitoredValues, rekorClient *client.Rekor, outputIdentitiesFile string) error { + // Get log size of inactive shards + startIndex, endIndex := GetCheckpointIndices(logInfo, prevCheckpoint, checkpoint) + + // Search for identities in the log range + if identity.MonitoredValuesExist(monitoredValues) { + err := IdentitySearch(startIndex, endIndex, rekorClient, monitoredValues, outputIdentitiesFile) + if err != nil { + return fmt.Errorf("error monitoring for identities: %v", err) + } + } + return nil +} diff --git a/pkg/rekor/verifier.go b/pkg/rekor/verifier.go index eaec220..4864465 100644 --- a/pkg/rekor/verifier.go +++ b/pkg/rekor/verifier.go @@ -51,11 +51,21 @@ func GetLogVerifier(ctx context.Context, rekorClient *client.Rekor) (signature.V return verifier, nil } +// ReadLatestCheckpoint fetches the latest checkpoint from log info fetched from Rekor. +// It returns the checkpoint if it successfully fetches one; otherwise, it returns an error. +func ReadLatestCheckpoint(logInfo *models.LogInfo) (*util.SignedCheckpoint, error) { + checkpoint := &util.SignedCheckpoint{} + if err := checkpoint.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + return nil, fmt.Errorf("unmarshalling logInfo.SignedTreeHead to Checkpoint: %v", err) + } + return checkpoint, nil +} + // verifyLatestCheckpoint fetches and verifies the signature of the latest checkpoint from log info fetched from Rekor. // If it successfully verifies the checkpoint's signature, it returns the checkpoint; otherwise, it returns an error. func verifyLatestCheckpointSignature(logInfo *models.LogInfo, verifier signature.Verifier) (*util.SignedCheckpoint, error) { - checkpoint := &util.SignedCheckpoint{} - if err := checkpoint.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + checkpoint, err := ReadLatestCheckpoint(logInfo) + if err != nil { return nil, fmt.Errorf("unmarshalling logInfo.SignedTreeHead to Checkpoint: %v", err) } if !checkpoint.Verify(verifier) { diff --git a/pkg/test/identity_workflow/identity_workflow_e2e_test.go b/pkg/test/identity_workflow/identity_workflow_e2e_test.go new file mode 100644 index 0000000..0cc3588 --- /dev/null +++ b/pkg/test/identity_workflow/identity_workflow_e2e_test.go @@ -0,0 +1,224 @@ +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build e2e +// +build e2e + +package e2e + +import ( + "bytes" + "context" + "crypto" + "crypto/sha256" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "fmt" + "log" + "os" + "runtime" + "strings" + "testing" + + "github.com/sigstore/rekor-monitor/pkg/fulcio/extensions" + "github.com/sigstore/rekor-monitor/pkg/identity" + "github.com/sigstore/rekor-monitor/pkg/rekor" + "github.com/sigstore/rekor-monitor/pkg/test" + "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/rekor/pkg/util" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "sigs.k8s.io/release-utils/version" + + hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" +) + +const ( + rekorURL = "http://127.0.0.1:3000" + subject = "subject@example.com" + issuer = "oidc-issuer@domain.com" + extValueString = "test cert value" +) + +// Test IdentitySearch: +// Check that Rekor-monitor reusable identity search workflow successfully +// finds a monitored identity within the checkpoint indices and writes it to file. +func TestIdentitySearch(t *testing.T) { + rekorClient, err := client.GetRekorClient(rekorURL, client.WithUserAgent(strings.TrimSpace(fmt.Sprintf("rekor-monitor/%s (%s; %s)", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH)))) + if err != nil { + log.Fatalf("getting Rekor client: %v", err) + } + + oid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 9} + extValue, err := asn1.Marshal(extValueString) + if err != nil { + t.Fatal(err) + } + extension := pkix.Extension{ + Id: oid, + Critical: false, + Value: extValue, + } + + rootCert, rootKey, _ := test.GenerateRootCA() + leafCert, leafKey, _ := test.GenerateLeafCert(subject, issuer, rootCert, rootKey, extension) + + signer, err := signature.LoadECDSASignerVerifier(leafKey, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + pemCert, _ := cryptoutils.MarshalCertificateToPEM(leafCert) + + payload := []byte{1, 2, 3, 4} + sig, err := signer.SignMessage(bytes.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + + hashedrekord := &hashedrekord_v001.V001Entry{} + hash := sha256.Sum256(payload) + pe, err := hashedrekord.CreateFromArtifactProperties(context.Background(), types.ArtifactProperties{ + ArtifactHash: hex.EncodeToString(hash[:]), + SignatureBytes: sig, + PublicKeyBytes: [][]byte{pemCert}, + PKIFormat: "x509", + }) + if err != nil { + t.Fatalf("error creating hashed rekord entry: %v", err) + } + + x509Cert, err := cryptoutils.UnmarshalCertificatesFromPEM(pemCert) + if err != nil { + t.Fatal(err) + } + digest := sha256.Sum256(x509Cert[0].Raw) + certFingerprint := hex.EncodeToString(digest[:]) + + params := entries.NewCreateLogEntryParams() + params.SetProposedEntry(pe) + resp, err := rekorClient.Entries.CreateLogEntry(params) + if !resp.IsSuccess() || err != nil { + t.Errorf("error creating log entry: %v", err) + } + + logInfo, err := rekor.GetLogInfo(context.Background(), rekorClient) + if err != nil { + t.Errorf("error getting log info: %v", err) + } + checkpoint := &util.SignedCheckpoint{} + if err := checkpoint.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + t.Errorf("%v", err) + } + if checkpoint.Size != 1 { + t.Errorf("expected checkpoint size of 1, received size %d", checkpoint.Size) + } + + tempDir := t.TempDir() + tempLogInfoFile, err := os.CreateTemp(tempDir, "") + if err != nil { + t.Errorf("failed to create temp log file: %v", err) + } + tempLogInfoFileName := tempLogInfoFile.Name() + defer os.Remove(tempLogInfoFileName) + + tempOutputIdentitiesFile, err := os.CreateTemp(tempDir, "") + if err != nil { + t.Errorf("failed to create temp output identities file: %v", err) + } + tempOutputIdentitiesFileName := tempOutputIdentitiesFile.Name() + defer os.Remove(tempOutputIdentitiesFileName) + + monitoredVals := identity.MonitoredValues{ + Subjects: []string{subject}, + CertificateIdentities: []identity.CertificateIdentity{ + { + CertSubject: ".*ubje.*", + Issuers: []string{".+@domain.com"}, + }, + }, + OIDMatchers: []extensions.OIDMatcher{ + { + ObjectIdentifier: oid, + ExtensionValues: []string{extValueString}, + }, + }, + Fingerprints: []string{ + certFingerprint, + }, + } + + payload = []byte{1, 2, 3, 4, 5, 6} + sig, err = signer.SignMessage(bytes.NewReader(payload)) + if err != nil { + t.Fatalf("error signing message: %v", err) + } + hashedrekord = &hashedrekord_v001.V001Entry{} + hash = sha256.Sum256(payload) + pe, err = hashedrekord.CreateFromArtifactProperties(context.Background(), types.ArtifactProperties{ + ArtifactHash: hex.EncodeToString(hash[:]), + SignatureBytes: sig, + PublicKeyBytes: [][]byte{pemCert}, + PKIFormat: "x509", + }) + if err != nil { + t.Fatalf("error creating hashed rekord log entry: %v", err) + } + params = entries.NewCreateLogEntryParams() + params.SetProposedEntry(pe) + resp, err = rekorClient.Entries.CreateLogEntry(params) + if !resp.IsSuccess() || err != nil { + t.Errorf("error creating log entry: %v", err) + } + + logInfo, err = rekor.GetLogInfo(context.Background(), rekorClient) + if err != nil { + t.Errorf("error getting log info: %v", err) + } + checkpoint = &util.SignedCheckpoint{} + if err := checkpoint.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil { + t.Errorf("%v", err) + } + if checkpoint.Size != 2 { + t.Errorf("expected checkpoint size of 2, received size %d", checkpoint.Size) + } + + err = rekor.IdentitySearch(0, 1, rekorClient, monitoredVals, tempOutputIdentitiesFileName) + if err != nil { + log.Fatal(err.Error()) + } + + tempOutputIdentities, err := os.ReadFile(tempOutputIdentitiesFileName) + if err != nil { + t.Errorf("error reading from output identities file: %v", err) + } + tempOutputIdentitiesString := string(tempOutputIdentities) + if !strings.Contains(tempOutputIdentitiesString, subject) { + t.Errorf("expected to find subject %s, did not", subject) + } + if !strings.Contains(tempOutputIdentitiesString, issuer) { + t.Errorf("expected to find issuer %s, did not", issuer) + } + if !strings.Contains(tempOutputIdentitiesString, oid.String()) { + t.Errorf("expected to find oid %s, did not", oid.String()) + } + if !strings.Contains(tempOutputIdentitiesString, oid.String()) { + t.Errorf("expected to find oid value %s, did not", extValueString) + } + if !strings.Contains(tempOutputIdentitiesString, certFingerprint) { + t.Errorf("expected to find fingerprint %s, did not", certFingerprint) + } +} diff --git a/pkg/test/identity_workflow/identity_workflow_e2e_test.sh b/pkg/test/identity_workflow/identity_workflow_e2e_test.sh new file mode 100755 index 0000000..f4106a9 --- /dev/null +++ b/pkg/test/identity_workflow/identity_workflow_e2e_test.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# +# Copyright 2024 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +pushd $HOME + +echo "downloading service repos" +for repo in rekor ; do + if [[ ! -d $repo ]]; then + git clone https://github.com/sigstore/${repo}.git + else + pushd $repo + git pull + popd + fi +done + +docker_compose="docker compose" + + +echo "starting services" +for repo in rekor ; do + pushd $repo + ${docker_compose} up -d + echo -n "waiting up to 60 sec for system to start" + count=0 + until [ $(${docker_compose} ps | grep -c "(healthy)") == 3 ]; + do + if [ $count -eq 6 ]; then + echo "! timeout reached" + exit 1 + else + echo -n "." + sleep 10 + let 'count+=1' + fi + done + popd +done + +function cleanup_services() { + echo "cleaning up" + for repo in rekor; do + pushd $HOME/$repo + ${docker_compose} down + popd + done +} +trap cleanup_services EXIT + +echo +echo "running tests" + +popd +go test -tags=e2e -v -race ./pkg/test/identity_workflow/... \ No newline at end of file