diff --git a/.gitignore b/.gitignore index 37463fa6..a8e05fde 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ test/scorecard.json log sarif-report.json test/log +.idea/ diff --git a/attestation/github/github.go b/attestation/github/github.go index c768e3b7..46bce445 100644 --- a/attestation/github/github.go +++ b/attestation/github/github.go @@ -25,7 +25,6 @@ import ( "os" "strings" - "github.com/davecgh/go-spew/spew" "github.com/in-toto/go-witness/attestation" "github.com/in-toto/go-witness/attestation/jwt" "github.com/in-toto/go-witness/cryptoutil" @@ -51,18 +50,22 @@ var ( _ attestation.BackReffer = &Attestor{} ) +// init registers the github attestor. func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() }) } +// ErrNotGitlab is an error type that indicates the environment is not a github ci job. type ErrNotGitlab struct{} +// Error returns the error message for ErrNotGitlab. func (e ErrNotGitlab) Error() string { return "not in a github ci job" } +// Attestor is a struct that holds the necessary information for github attestation. type Attestor struct { JWT *jwt.Attestor `json:"jwt,omitempty"` CIConfigPath string `json:"ciconfigpath"` @@ -81,6 +84,7 @@ type Attestor struct { aud string } +// New creates and returns a new github attestor. func New() *Attestor { return &Attestor{ aud: tokenAudience, @@ -89,18 +93,22 @@ func New() *Attestor { } } +// Name returns the name of the attestor. func (a *Attestor) Name() string { return Name } +// Type returns the type of the attestor. func (a *Attestor) Type() string { return Type } +// RunType returns the run type of the attestor. func (a *Attestor) RunType() attestation.RunType { return RunType } +// Attest performs the attestation for the github environment. func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { if os.Getenv("GITHUB_ACTIONS") != "true" { return ErrNotGitlab{} @@ -108,16 +116,16 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { jwtString, err := fetchToken(a.tokenURL, os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"), "witness") if err != nil { - return err + return fmt.Errorf("error on fetching token %w", err) } - spew.Dump(jwtString) + if jwtString == "" { + return fmt.Errorf("empty JWT string") + } - if jwtString != "" { - a.JWT = jwt.New(jwt.WithToken(jwtString), jwt.WithJWKSUrl(a.jwksURL)) - if err := a.JWT.Attest(ctx); err != nil { - return err - } + a.JWT = jwt.New(jwt.WithToken(jwtString), jwt.WithJWKSUrl(a.jwksURL)) + if err := a.JWT.Attest(ctx); err != nil { + return fmt.Errorf("failed to attest github jwt: %w", err) } a.CIServerUrl = os.Getenv("GITHUB_SERVER_URL") @@ -134,6 +142,7 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { return nil } +// Subjects returns a map of subjects and their corresponding digest sets. func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects := make(map[string]cryptoutil.DigestSet) hashes := []crypto.Hash{crypto.SHA256} @@ -152,6 +161,7 @@ func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { return subjects } +// BackRefs returns a map of back references and their corresponding digest sets. func (a *Attestor) BackRefs() map[string]cryptoutil.DigestSet { backRefs := make(map[string]cryptoutil.DigestSet) for subj, ds := range a.Subjects() { @@ -164,13 +174,14 @@ func (a *Attestor) BackRefs() map[string]cryptoutil.DigestSet { return backRefs } +// fetchToken fetches the token from the given URL. func fetchToken(tokenURL string, bearer string, audience string) (string, error) { client := &http.Client{} - //add audient "&audience=witness" to the end of the tokenURL, parse it, and then add it to the query + // add audience "&audience=witness" to the end of the tokenURL, parse it, and then add it to the query u, err := url.Parse(tokenURL) if err != nil { - return "", err + return "", fmt.Errorf("error on parsing token url %w", err) } q := u.Query() @@ -181,33 +192,35 @@ func fetchToken(tokenURL string, bearer string, audience string) (string, error) req, err := http.NewRequest("GET", reqURL, nil) if err != nil { - return "", err + return "", fmt.Errorf("error on creating request %w", err) } req.Header.Add("Authorization", "bearer "+bearer) resp, err := client.Do(req) if err != nil { - return "", err + return "", fmt.Errorf("error on request %w", err) } defer resp.Body.Close() body, err := readResponseBody(resp.Body) if err != nil { - return "", err + return "", fmt.Errorf("error on reading response body %w", err) } var tokenResponse GithubTokenResponse err = json.Unmarshal(body, &tokenResponse) if err != nil { - return "", err + return "", fmt.Errorf("error on unmarshaling token response %w", err) } return tokenResponse.Value, nil } +// GithubTokenResponse is a struct that holds the response from the github token request. type GithubTokenResponse struct { Count int `json:"count"` Value string `json:"value"` } +// readResponseBody reads the response body and returns it as a byte slice. func readResponseBody(body io.Reader) ([]byte, error) { var buf bytes.Buffer _, err := buf.ReadFrom(body) diff --git a/attestation/github/github_test.go b/attestation/github/github_test.go new file mode 100644 index 00000000..d2ac0e8f --- /dev/null +++ b/attestation/github/github_test.go @@ -0,0 +1,122 @@ +// Copyright 2021 The Witness Contributors +// +// 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 github + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func createMockServer() *httptest.Server { + type Response struct { + Count int `json:"count"` + Value string `json:"value"` + } + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/valid" && r.Header.Get("Authorization") == "bearer validBearer" { + resp, _ := json.Marshal(Response{Count: 1, Value: "validJWTToken"}) + _, _ = w.Write(resp) + } else { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + } + })) +} + +func TestFetchToken(t *testing.T) { + testCases := []struct { + name string + tokenURL string + bearer string + audience string + wantToken string + wantErr bool + }{ + { + name: "valid token", + tokenURL: "/valid", + bearer: "validBearer", + audience: "validAudience", + wantToken: "validJWTToken", + wantErr: false, + }, + { + name: "invalid token url", + tokenURL: "/invalid", + bearer: "validBearer", + audience: "validAudience", + wantToken: "", + wantErr: true, + }, + { + name: "invalid bearer", + tokenURL: "/valid", + bearer: "invalidBearer", + audience: "validAudience", + wantToken: "", + wantErr: true, + }, + { + name: "invalid url", + tokenURL: "invalidURL", + bearer: "validBearer", + audience: "validAudience", + wantToken: "", + wantErr: true, + }, + } + + server := createMockServer() + defer server.Close() + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + gotToken, err := fetchToken(server.URL+testCase.tokenURL, testCase.bearer, testCase.audience) + if testCase.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.wantToken, gotToken) + } + }) + } +} + +func TestSubjects(t *testing.T) { + tokenServer := createMockServer() + defer tokenServer.Close() + attestor := &Attestor{ + aud: "projecturl", + jwksURL: tokenServer.URL, + tokenURL: os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"), + } + + subjects := attestor.Subjects() + assert.NotNil(t, subjects) + assert.Equal(t, 2, len(subjects)) + + expectedSubjects := []string{"pipelineurl:" + attestor.PipelineUrl, "projecturl:" + attestor.ProjectUrl} + for _, expectedSubject := range expectedSubjects { + _, ok := subjects[expectedSubject] + assert.True(t, ok, "Expected subject not found: %s", expectedSubject) + } + m := attestor.BackRefs() + assert.NotNil(t, m) + assert.Equal(t, 1, len(m)) +} diff --git a/attestation/jwt/jwt.go b/attestation/jwt/jwt.go index 1eb2ca12..02986308 100644 --- a/attestation/jwt/jwt.go +++ b/attestation/jwt/jwt.go @@ -93,23 +93,23 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { parsed, err := jwt.ParseSigned(a.token) if err != nil { - return err + return fmt.Errorf("error parsing token: %w", err) } resp, err := http.Get(a.jwksUrl) if err != nil { - return err + return fmt.Errorf("error fetching jwks: %w", err) } defer resp.Body.Close() jwks := jose.JSONWebKeySet{} decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&jwks); err != nil { - return err + return fmt.Errorf("error decoding jwks: %w", err) } if err := parsed.Claims(jwks, &a.Claims); err != nil { - return err + return fmt.Errorf("error parsing claims: %w", err) } keyID := "" diff --git a/attestation/maven/maven.go b/attestation/maven/maven.go index 9e50034d..6392c35a 100644 --- a/attestation/maven/maven.go +++ b/attestation/maven/maven.go @@ -24,12 +24,14 @@ import ( "github.com/in-toto/go-witness/attestation" "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/log" + "github.com/in-toto/go-witness/registry" ) const ( - Name = "maven" - Type = "https://witness.dev/attestations/maven/v0.1" - RunType = attestation.PreMaterialRunType + Name = "maven" + Type = "https://witness.dev/attestations/maven/v0.1" + RunType = attestation.PreMaterialRunType + defaultPomPath = "pom.xml" ) // This is a hacky way to create a compile time error in case the attestor @@ -42,7 +44,22 @@ var ( func init() { attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { return New() - }) + }, + registry.StringConfigOption( + "pom-path", + fmt.Sprintf("The path to the Project Object Model (POM) XML file used for task being attested (default \"%s\").", defaultPomPath), + defaultPomPath, + func(a attestation.Attestor, pomPath string) (attestation.Attestor, error) { + mavAttestor, ok := a.(*Attestor) + if !ok { + return a, fmt.Errorf("unexpected attestor type: %T is not a maven attestor", a) + } + + WithPom(pomPath)(mavAttestor) + return mavAttestor, nil + }, + ), + ) } type Attestor struct { @@ -73,7 +90,7 @@ func WithPom(path string) Option { func New(opts ...Option) *Attestor { attestor := &Attestor{ - pomPath: "pom.xml", + pomPath: defaultPomPath, } for _, opt := range opts { diff --git a/attestation/maven/maven_test.go b/attestation/maven/maven_test.go index 9934433e..8e67ccd8 100644 --- a/attestation/maven/maven_test.go +++ b/attestation/maven/maven_test.go @@ -20,13 +20,12 @@ import ( "testing" "github.com/in-toto/go-witness/attestation" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func writeTempPomXml(t *testing.T) (string, error) { +func writeTempPomXml(t *testing.T, path string) (string, error) { tmpDir := t.TempDir() - pomPath := filepath.Join(tmpDir, "pom.xml") + pomPath := filepath.Join(tmpDir, path) file, err := os.Create(pomPath) if err != nil { return "", err @@ -41,13 +40,39 @@ func writeTempPomXml(t *testing.T) (string, error) { } func TestMaven(t *testing.T) { - pomPath, err := writeTempPomXml(t) - require.NoError(t, err) - attestor := New(WithPom(pomPath)) - ctx, err := attestation.NewContext([]attestation.Attestor{attestor}) - require.NoError(t, err) - err = attestor.Attest(ctx) - assert.NoError(t, err) + workingDir := t.TempDir() + + tests := []struct { + name string + pomPath string + }{ + {"no pom specified", ""}, + {"regular pom with custom name", "custom-pom.xml"}, + {"effective pom", "effective-pom.xml"}, + } + + for _, test := range tests { + var p string + var err error + if test.pomPath != "" { + p, err = writeTempPomXml(t, test.pomPath) + if err != nil { + t.Fatal(err) + } + } else { + p, err = writeTempPomXml(t, "pom.xml") + if err != nil { + t.Fatal(err) + } + } + + t.Run(test.name, func(t *testing.T) { + ctx, err := attestation.NewContext([]attestation.Attestor{}, attestation.WithWorkingDir(workingDir)) + require.NoError(t, err) + a := New(WithPom(p)) + require.NoError(t, a.Attest(ctx)) + }) + } } const testPomXml = ` diff --git a/dsse/dsse.go b/dsse/dsse.go index 34d4796f..65a16926 100644 --- a/dsse/dsse.go +++ b/dsse/dsse.go @@ -32,11 +32,11 @@ func (e ErrNoMatchingSigs) Error() string { type ErrThresholdNotMet struct { Theshold int - Acutal int + Actual int } func (e ErrThresholdNotMet) Error() string { - return fmt.Sprintf("envelope did not meet verifier threshold. expected %v valid verifiers but got %v", e.Theshold, e.Acutal) + return fmt.Sprintf("envelope did not meet verifier threshold. expected %v valid verifiers but got %v", e.Theshold, e.Actual) } type ErrInvalidThreshold int diff --git a/dsse/dsse_test.go b/dsse/dsse_test.go index 7a63e251..a23b01f2 100644 --- a/dsse/dsse_test.go +++ b/dsse/dsse_test.go @@ -217,7 +217,7 @@ func TestThreshold(t *testing.T) { assert.ElementsMatch(t, approvedVerifiers, expectedVerifiers) approvedVerifiers, err = env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(10)) - require.ErrorIs(t, err, ErrThresholdNotMet{Acutal: 5, Theshold: 10}) + require.ErrorIs(t, err, ErrThresholdNotMet{Actual: 5, Theshold: 10}) assert.ElementsMatch(t, approvedVerifiers, expectedVerifiers) _, err = env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(-10)) diff --git a/dsse/verify.go b/dsse/verify.go index b74c24ed..328a23fd 100644 --- a/dsse/verify.go +++ b/dsse/verify.go @@ -22,6 +22,7 @@ import ( "time" "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/log" ) type TimestampVerifier interface { @@ -115,6 +116,8 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { if verifier, err := verifyX509Time(cert, sigIntermediates, options.roots, pae, sig.Signature, time.Now()); err == nil { matchingSigFound = true passedVerifiers = append(passedVerifiers, PassedVerifier{Verifier: verifier}) + } else { + log.Debugf("failed to verify with timestamp verifier: %w", err) } } else { var passedVerifier cryptoutil.Verifier @@ -130,7 +133,10 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { if verifier, err := verifyX509Time(cert, sigIntermediates, options.roots, pae, sig.Signature, timestamp); err == nil { passedVerifier = verifier passedTimestampVerifiers = append(passedTimestampVerifiers, timestampVerifier) + } else { + log.Debugf("failed to verify with timestamp verifier: %w", err) } + } } @@ -159,7 +165,7 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { } if len(passedVerifiers) < options.threshold { - return passedVerifiers, ErrThresholdNotMet{Theshold: options.threshold, Acutal: len(passedVerifiers)} + return passedVerifiers, ErrThresholdNotMet{Theshold: options.threshold, Actual: len(passedVerifiers)} } return passedVerifiers, nil diff --git a/source/memory_test.go b/source/memory_test.go new file mode 100644 index 00000000..f4a16cd6 --- /dev/null +++ b/source/memory_test.go @@ -0,0 +1,317 @@ +// Copyright 2021 The Witness Contributors +// +// 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 source + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/dsse" + intoto "github.com/in-toto/go-witness/intoto" +) + +func TestLoadEnvelope(t *testing.T) { + // Marshal the attestation.Collection into a JSON byte array + predicate, err := json.Marshal(attestation.Collection{}) + if err != nil { + t.Fatalf("failed to marshal predicate, err = %v", err) + } + + // Define the test cases + tests := []struct { + name string + reference string + intotoStatment intoto.Statement + mSource *MemorySource + attCol attestation.Collection + wantLoadEnvelopeErr bool + wantPredicateErr bool + wantMemorySourceErr bool + wantReferenceExistErr bool + }{ + { + name: "Valid intotoStatment", + reference: "ref", + intotoStatment: intoto.Statement{ + Type: "https://in-toto.io/Statement/v0.1", + Subject: []intoto.Subject{{Name: "example", Digest: map[string]string{"sha256": "exampledigest"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(predicate), + }, + attCol: attestation.Collection{}, + mSource: NewMemorySource(), + }, + { + name: "Empty intotoStatment", + reference: "ref", + intotoStatment: intoto.Statement{}, + mSource: NewMemorySource(), + attCol: attestation.Collection{}, + wantPredicateErr: true, + wantMemorySourceErr: true, + }, + { + name: "Invalid intotoStatment Predicate", + reference: "ref", + intotoStatment: intoto.Statement{ + Type: "https://in-toto.io/Statement/v0.1", + Subject: []intoto.Subject{{Name: "example", Digest: map[string]string{"sha256": "exampledigest"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage("invalid-predicate"), + }, + attCol: attestation.Collection{}, + mSource: NewMemorySource(), + wantLoadEnvelopeErr: true, + wantMemorySourceErr: true, + }, + { + name: "Valid intotoStatment", + reference: "ref", + intotoStatment: intoto.Statement{ + Type: "https://in-toto.io/Statement/v0.1", + Subject: []intoto.Subject{{Name: "example", Digest: map[string]string{"sha256": "exampledigest"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(predicate), + }, + mSource: NewMemorySource(), + wantLoadEnvelopeErr: true, + wantReferenceExistErr: true, + }, + } + + // Run the test cases + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Marshal the intoto.Statement into a JSON byte array + statementBytes, _ := json.Marshal(tt.intotoStatment) + + // Create a new dsse.Envelope with the marshalled intoto.Statement as the payload + envelope := dsse.Envelope{ + Payload: statementBytes, + PayloadType: "application/vnd.in-toto+json", + } + + // Initialize a new MemorySource + memorySource := NewMemorySource() + if tt.wantReferenceExistErr { + collEnv, err := envelopeToCollectionEnvelope(tt.reference, envelope) + if err != nil { + t.Fatalf("Invalid intotoStatment, err = %v", err) + } + // since this envelope is not in the MemorySource, we can add the collection envelope into the map + memorySource.envelopesByReference[tt.reference] = collEnv + } + + // Load the dsse.Envelope into the MemorySource + err = memorySource.LoadEnvelope(tt.reference, envelope) + if err != nil { + // if we did not want the error + if !tt.wantLoadEnvelopeErr { + t.Fatalf("LoadEnvelope() error = %v, wantErr %v", err, tt.wantLoadEnvelopeErr) + } + return + + } + + // Check if the loaded envelope matches the expected CollectionEnvelope + + expectedCollectionEnvelope := CollectionEnvelope{ + Envelope: envelope, + Statement: tt.intotoStatment, + Collection: tt.attCol, + Reference: tt.reference, + } + if !reflect.DeepEqual(memorySource.envelopesByReference[tt.reference], expectedCollectionEnvelope) != tt.wantMemorySourceErr { + t.Fatalf("Mismatch or non-existence of collection envelope for reference in envelopesByReference map.") + } + // Verify if the subjects and attestations are present in the loaded envelope + for _, sub := range tt.intotoStatment.Subject { + for _, digest := range sub.Digest { + if _, ok := memorySource.subjectDigestsByReference[tt.reference][digest]; !ok != tt.wantMemorySourceErr { + t.Fatalf("memorySource does not contain passed in digest = %v", digest) + } + } + } + for _, att := range tt.attCol.Attestations { + if _, ok := memorySource.attestationsByReference[tt.reference][att.Attestation.Type()]; !ok != tt.wantMemorySourceErr { + t.Fatalf("memorySource does not contain passed in attestation = %v", att.Attestation.Name()) + } + } + }) + } +} + +func TestSearch(t *testing.T) { + // Marshal the attestation.Collection into a JSON byte array + validPredicate, err := json.Marshal(attestation.Collection{Name: "t"}) + if err != nil { + t.Fatalf("failed to marshal predicate, err = %v", err) + } + + // Define the arguments for the test cases + type args struct { + ctx context.Context + collectionName string + subDigest []string + attestations []string + } + // Define the test cases + tests := []struct { + name string + statements []intoto.Statement + searchQuery args + wantReferences map[string]struct{} + wantErr bool + }{ + { + name: "all match given query", + statements: []intoto.Statement{ + { + Type: "1", + Subject: []intoto.Subject{{Name: "example1", Digest: map[string]string{"a": "exampledigest", "b": "exampledigest2", "c": "exampledigest3"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "2", + Subject: []intoto.Subject{{Name: "example2", Digest: map[string]string{"a": "exampledigest", "b": "exampledigest2"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "3", + Subject: []intoto.Subject{{Name: "example3", Digest: map[string]string{"a": "exampledigest"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + }, + searchQuery: args{ + collectionName: "t", + subDigest: []string{"exampledigest", "notincluded"}, + attestations: []string{}, + }, + wantReferences: map[string]struct{}{"ref0": {}, "ref1": {}, "ref2": {}}, + wantErr: false, + }, + { + name: "some match", + statements: []intoto.Statement{ + { + Type: "1", + Subject: []intoto.Subject{{Name: "example1", Digest: map[string]string{"a": "exampledigest", "b": "exampledigest2", "c": "exampledigest3"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "2", + Subject: []intoto.Subject{{Name: "example2", Digest: map[string]string{"a": "exampledigest", "b": "exampledigest2"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "3", + Subject: []intoto.Subject{{Name: "example3", Digest: map[string]string{"a": "exampledigest"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "4", + Subject: []intoto.Subject{{Name: "example1", Digest: map[string]string{"a": "not included"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + }, + searchQuery: args{ + collectionName: "t", + subDigest: []string{"exampledigest"}, + attestations: []string{}, + }, + wantReferences: map[string]struct{}{"ref0": {}, "ref1": {}, "ref2": {}}, + wantErr: false, + }, + { + name: "no matches", + statements: []intoto.Statement{ + { + Type: "1", + Subject: []intoto.Subject{{Name: "example1", Digest: map[string]string{"a": "exampledigest", "b": "exampledigest2", "c": "exampledigest3"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "2", + Subject: []intoto.Subject{{Name: "example2", Digest: map[string]string{"a": "exampledigest", "b": "exampledigest2"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + { + Type: "3", + Subject: []intoto.Subject{{Name: "example3", Digest: map[string]string{"a": "exampledigest"}}}, + PredicateType: "https://witness.testifysec.com/attestation-collection/v0.1", + Predicate: json.RawMessage(validPredicate), + }, + }, + searchQuery: args{ + collectionName: "t", + subDigest: []string{}, + attestations: []string{}, + }, + wantReferences: map[string]struct{}{}, + wantErr: false, + }, + } + + // Run the test cases + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize a new MemorySource + s := NewMemorySource() + expectedResult := []CollectionEnvelope{} + for i := range tt.statements { + // Marshal the intoto.Statement into a JSON byte array + payload, _ := json.Marshal(tt.statements[i]) + // Create a new dsse.Envelope with the marshalled intoto.Statement as the payload + dsseEnv := dsse.Envelope{ + Payload: payload, + PayloadType: "application/vnd.in-toto+json", + } + // Load the dsse.Envelope into the MemorySource + err := s.LoadEnvelope("ref"+fmt.Sprint(i), dsseEnv) + if err != nil { + t.Fatalf("invalid intoto statment, err = %v", err) + } + + if _, ok := tt.wantReferences["ref"+fmt.Sprint(i)]; ok { + collEnv, _ := envelopeToCollectionEnvelope("ref"+fmt.Sprint(i), dsseEnv) + expectedResult = append(expectedResult, collEnv) + } + } + + // Run the search query on the MemorySource + got, err := s.Search(tt.searchQuery.ctx, tt.searchQuery.collectionName, tt.searchQuery.subDigest, tt.searchQuery.attestations) + if (err != nil) != tt.wantErr { + t.Fatalf("MemorySource.Search() error = %v, wantErr %v", err, tt.wantErr) + } + // Check if the search results match the expected results + if !reflect.DeepEqual(got, expectedResult) { + t.Fatalf("MemorySource.Search() = %v, want %v", got, expectedResult) + } + }) + } +} diff --git a/verify.go b/verify.go index bb20df6b..68cd5c76 100644 --- a/verify.go +++ b/verify.go @@ -40,10 +40,13 @@ func VerifySignature(r io.Reader, verifiers ...cryptoutil.Verifier) (dsse.Envelo } type verifyOptions struct { - policyEnvelope dsse.Envelope - policyVerifiers []cryptoutil.Verifier - collectionSource source.Sourcer - subjectDigests []string + policyTimestampAuthorities []dsse.TimestampVerifier + policyCARoots []*x509.Certificate + policyCAIntermediates []*x509.Certificate + policyEnvelope dsse.Envelope + policyVerifiers []cryptoutil.Verifier + collectionSource source.Sourcer + subjectDigests []string } type VerifyOption func(*verifyOptions) @@ -64,6 +67,18 @@ func VerifyWithCollectionSource(source source.Sourcer) VerifyOption { } } +func VerifyWithPolicyTimestampAuthorities(authorities []dsse.TimestampVerifier) VerifyOption { + return func(vo *verifyOptions) { + vo.policyTimestampAuthorities = authorities + } +} + +func VerifyWithPolicyCARoots(roots []*x509.Certificate) VerifyOption { + return func(vo *verifyOptions) { + vo.policyCARoots = roots + } +} + // Verify verifies a set of attestations against a provided policy. The set of attestations that satisfy the policy will be returned // if verifiation is successful. func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers []cryptoutil.Verifier, opts ...VerifyOption) (map[string][]source.VerifiedCollection, error) { @@ -76,7 +91,7 @@ func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers [ opt(&vo) } - if _, err := vo.policyEnvelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...)); err != nil { + if _, err := vo.policyEnvelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...), dsse.VerifyWithTimestampVerifiers(vo.policyTimestampAuthorities...), dsse.VerifyWithRoots(vo.policyCARoots...), dsse.VerifyWithIntermediates(vo.policyCAIntermediates...)); err != nil { return nil, fmt.Errorf("could not verify policy: %w", err) }