diff --git a/dsse/dsse.go b/dsse/dsse.go index 65a16926..aa6b5d24 100644 --- a/dsse/dsse.go +++ b/dsse/dsse.go @@ -16,6 +16,8 @@ package dsse import ( "fmt" + + "github.com/in-toto/go-witness/log" ) type ErrNoSignatures struct{} @@ -24,10 +26,25 @@ func (e ErrNoSignatures) Error() string { return "no signatures in dsse envelope" } -type ErrNoMatchingSigs struct{} +type ErrNoMatchingSigs struct { + Verifiers []CheckedVerifier +} func (e ErrNoMatchingSigs) Error() string { - return "no valid signatures for the provided verifiers found" + mess := "no valid signatures for the provided verifiers found for keyids:\n" + for _, v := range e.Verifiers { + if v.Error != nil { + kid, err := v.Verifier.KeyID() + if err != nil { + log.Warn("failed to get key id from verifier: %v", err) + } + + s := fmt.Sprintf(" %s: %v\n", kid, v.Error) + mess += s + } + } + + return mess } type ErrThresholdNotMet struct { diff --git a/dsse/dsse_test.go b/dsse/dsse_test.go index 3cd88772..0e77c72d 100644 --- a/dsse/dsse_test.go +++ b/dsse/dsse_test.go @@ -152,7 +152,7 @@ func TestVerify(t *testing.T) { env, err := Sign("dummydata", bytes.NewReader([]byte("this is some dummy data")), SignWithSigners(signer)) require.NoError(t, err) approvedVerifiers, err := env.Verify(VerifyWithVerifiers(verifier)) - assert.ElementsMatch(t, approvedVerifiers, []PassedVerifier{{Verifier: verifier}}) + assert.ElementsMatch(t, approvedVerifiers, []CheckedVerifier{{Verifier: verifier}}) require.NoError(t, err) } @@ -165,38 +165,44 @@ func TestFailVerify(t *testing.T) { require.NoError(t, err) approvedVerifiers, err := env.Verify(VerifyWithVerifiers(verifier)) assert.Empty(t, approvedVerifiers) - require.ErrorIs(t, err, ErrNoMatchingSigs{}) + require.ErrorAs(t, err, &ErrNoMatchingSigs{Verifiers: []CheckedVerifier{{Verifier: verifier, Error: rsa.ErrVerification}}}) } func TestMultiSigners(t *testing.T) { signers := []cryptoutil.Signer{} verifiers := []cryptoutil.Verifier{} - expectedVerifiers := []PassedVerifier{} + expectedVerifiers := []CheckedVerifier{} for i := 0; i < 5; i++ { s, v, err := createTestKey() require.NoError(t, err) signers = append(signers, s) verifiers = append(verifiers, v) - expectedVerifiers = append(expectedVerifiers, PassedVerifier{Verifier: v}) + expectedVerifiers = append(expectedVerifiers, CheckedVerifier{Verifier: v}) } env, err := Sign("dummydata", bytes.NewReader([]byte("this is some dummy data")), SignWithSigners(signers...)) require.NoError(t, err) - approvedVerifiers, err := env.Verify(VerifyWithVerifiers(verifiers...)) + checkedVerifiers, err := env.Verify(VerifyWithVerifiers(verifiers...)) + approvedVerifiers := []CheckedVerifier{} + for _, v := range checkedVerifiers { + if v.Error == nil { + approvedVerifiers = append(approvedVerifiers, v) + } + } require.NoError(t, err) assert.ElementsMatch(t, approvedVerifiers, expectedVerifiers) } func TestThreshold(t *testing.T) { signers := []cryptoutil.Signer{} - expectedVerifiers := []PassedVerifier{} + expectedVerifiers := []CheckedVerifier{} verifiers := []cryptoutil.Verifier{} for i := 0; i < 5; i++ { s, v, err := createTestKey() require.NoError(t, err) signers = append(signers, s) - expectedVerifiers = append(expectedVerifiers, PassedVerifier{Verifier: v}) + expectedVerifiers = append(expectedVerifiers, CheckedVerifier{Verifier: v}) verifiers = append(verifiers, v) } @@ -210,12 +216,26 @@ func TestThreshold(t *testing.T) { env, err := Sign("dummydata", bytes.NewReader([]byte("this is some dummy data")), SignWithSigners(signers...)) require.NoError(t, err) - approvedVerifiers, err := env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(5)) + checkedVerifiers, err := env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(5)) require.NoError(t, err) + + approvedVerifiers := []CheckedVerifier{} + for _, v := range checkedVerifiers { + if v.Error == nil { + approvedVerifiers = append(approvedVerifiers, v) + } + } assert.ElementsMatch(t, approvedVerifiers, expectedVerifiers) - approvedVerifiers, err = env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(10)) + checkedVerifiers, err = env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(10)) require.ErrorIs(t, err, ErrThresholdNotMet{Actual: 5, Theshold: 10}) + + approvedVerifiers = []CheckedVerifier{} + for _, v := range checkedVerifiers { + if v.Error == nil { + approvedVerifiers = append(approvedVerifiers, v) + } + } assert.ElementsMatch(t, approvedVerifiers, expectedVerifiers) _, err = env.Verify(VerifyWithVerifiers(verifiers...), VerifyWithThreshold(-10)) @@ -257,9 +277,16 @@ func TestTimestamp(t *testing.T) { env, err := Sign("dummydata", bytes.NewReader([]byte("this is some dummy data")), SignWithSigners(s), SignWithTimestampers(allTimestampers...)) require.NoError(t, err) - approvedVerifiers, err := env.Verify(VerifyWithVerifiers(v), VerifyWithRoots(root), VerifyWithIntermediates(intermediate), VerifyWithTimestampVerifiers(allTimestampVerifiers...)) + checkedVerifiers, err := env.Verify(VerifyWithVerifiers(v), VerifyWithRoots(root), VerifyWithIntermediates(intermediate), VerifyWithTimestampVerifiers(allTimestampVerifiers...)) require.NoError(t, err) + + approvedVerifiers := []CheckedVerifier{} + for _, v := range checkedVerifiers { + if v.Error == nil { + approvedVerifiers = append(approvedVerifiers, v) + } + } assert.Len(t, approvedVerifiers, 1) - assert.Len(t, approvedVerifiers[0].PassedTimestampVerifiers, len(expectedTimestampers)) - assert.ElementsMatch(t, approvedVerifiers[0].PassedTimestampVerifiers, expectedTimestampers) + assert.Len(t, approvedVerifiers[0].TimestampVerifiers, len(expectedTimestampers)) + assert.ElementsMatch(t, approvedVerifiers[0].TimestampVerifiers, expectedTimestampers) } diff --git a/dsse/verify.go b/dsse/verify.go index b028b8a6..b734f317 100644 --- a/dsse/verify.go +++ b/dsse/verify.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "crypto/x509" + "fmt" "time" "github.com/in-toto/go-witness/cryptoutil" @@ -65,12 +66,15 @@ func VerifyWithTimestampVerifiers(verifiers ...timestamp.TimestampVerifier) Veri } } -type PassedVerifier struct { - Verifier cryptoutil.Verifier - PassedTimestampVerifiers []timestamp.TimestampVerifier +type CheckedVerifier struct { + Verifier cryptoutil.Verifier + TimestampVerifiers []timestamp.TimestampVerifier + Error error } -func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { +type FailedVerifier struct{} + +func (e Envelope) Verify(opts ...VerificationOption) ([]CheckedVerifier, error) { options := &verificationOptions{ threshold: 1, } @@ -88,8 +92,8 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { return nil, ErrNoSignatures{} } - matchingSigFound := false - passedVerifiers := make([]PassedVerifier, 0) + checkedVerifiers := make([]CheckedVerifier, 0) + verified := 0 for _, sig := range e.Signatures { if sig.Certificate != nil && len(sig.Certificate) > 0 { cert, err := cryptoutil.TryParseCertificate(sig.Certificate) @@ -110,14 +114,17 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { sigIntermediates = append(sigIntermediates, options.intermediates...) if len(options.timestampVerifiers) == 0 { if verifier, err := verifyX509Time(cert, sigIntermediates, options.roots, pae, sig.Signature, time.Now()); err == nil { - matchingSigFound = true - passedVerifiers = append(passedVerifiers, PassedVerifier{Verifier: verifier}) + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier}) + verified += 1 } else { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier, Error: err}) log.Debugf("failed to verify with timestamp verifier: %w", err) } } else { var passedVerifier cryptoutil.Verifier + failed := []cryptoutil.Verifier{} passedTimestampVerifiers := []timestamp.TimestampVerifier{} + failedTimestampVerifiers := []timestamp.TimestampVerifier{} for _, timestampVerifier := range options.timestampVerifiers { for _, sigTimestamp := range sig.Timestamps { @@ -127,9 +134,12 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { } if verifier, err := verifyX509Time(cert, sigIntermediates, options.roots, pae, sig.Signature, timestamp); err == nil { + // NOTE: do we not want to save all the passed verifiers? passedVerifier = verifier passedTimestampVerifiers = append(passedTimestampVerifiers, timestampVerifier) } else { + failed = append(failed, verifier) + failedTimestampVerifiers = append(failedTimestampVerifiers, timestampVerifier) log.Debugf("failed to verify with timestamp verifier: %w", err) } @@ -137,34 +147,48 @@ func (e Envelope) Verify(opts ...VerificationOption) ([]PassedVerifier, error) { } if len(passedTimestampVerifiers) > 0 { - matchingSigFound = true - passedVerifiers = append(passedVerifiers, PassedVerifier{ - Verifier: passedVerifier, - PassedTimestampVerifiers: passedTimestampVerifiers, + verified += 1 + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{ + Verifier: passedVerifier, + TimestampVerifiers: passedTimestampVerifiers, }) + } else { + for _, v := range failed { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{ + Verifier: v, + TimestampVerifiers: failedTimestampVerifiers, + Error: fmt.Errorf("no valid timestamps found"), + }) + } } } } for _, verifier := range options.verifiers { if verifier != nil { + kid, err := verifier.KeyID() + if err != nil { + log.Warn("failed to get key id from verifier: %v", err) + } + log.Debug("verifying with verifier with KeyID ", kid) + if err := verifier.Verify(bytes.NewReader(pae), sig.Signature); err == nil { - passedVerifiers = append(passedVerifiers, PassedVerifier{Verifier: verifier}) - matchingSigFound = true + verified += 1 + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier}) + } else { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier, Error: err}) } } } } - if !matchingSigFound { - return nil, ErrNoMatchingSigs{} - } - - if len(passedVerifiers) < options.threshold { - return passedVerifiers, ErrThresholdNotMet{Theshold: options.threshold, Actual: len(passedVerifiers)} + if verified == 0 { + return nil, ErrNoMatchingSigs{Verifiers: checkedVerifiers} + } else if verified < options.threshold { + return checkedVerifiers, ErrThresholdNotMet{Theshold: options.threshold, Actual: verified} } - return passedVerifiers, nil + return checkedVerifiers, nil } func verifyX509Time(cert *x509.Certificate, sigIntermediates, roots []*x509.Certificate, pae, sig []byte, trustedTime time.Time) (cryptoutil.Verifier, error) { diff --git a/policy/errors.go b/policy/errors.go index 88f1800b..80ae4474 100644 --- a/policy/errors.go +++ b/policy/errors.go @@ -22,10 +22,27 @@ import ( "github.com/in-toto/go-witness/cryptoutil" ) -type ErrNoAttestations string +type ErrVerifyArtifactsFailed struct { + Reasons []string +} + +func (e ErrVerifyArtifactsFailed) Error() string { + mess := "failed to verify artifacts: \n" + for i, r := range e.Reasons { + if i == len(e.Reasons)-1 { + mess += r + "\n" + } + mess += r + ", \n" + } + return fmt.Sprintf("failed to verify artifacts: %v", e.Reasons) +} + +type ErrNoCollections struct { + Step string +} -func (e ErrNoAttestations) Error() string { - return fmt.Sprintf("no attestations found for step %v", string(e)) +func (e ErrNoCollections) Error() string { + return fmt.Sprintf("no collections found for step %v", e.Step) } type ErrMissingAttestation struct { @@ -89,7 +106,7 @@ type ErrPolicyDenied struct { } func (e ErrPolicyDenied) Error() string { - return fmt.Sprintf("policy was denied due to:\n%v", strings.Join(e.Reasons, "\n -")) + return fmt.Sprintf("policy was denied due to: %v", strings.Join(e.Reasons, ", ")) } type ErrConstraintCheckFailed struct { diff --git a/policy/policy.go b/policy/policy.go index e8828aea..b6bb9576 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -18,13 +18,13 @@ import ( "bytes" "context" "crypto/x509" + "errors" "fmt" "strings" "time" "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/signer/kms" "github.com/in-toto/go-witness/source" @@ -185,7 +185,7 @@ func checkVerifyOpts(vo *verifyOptions) error { return nil } -func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][]source.VerifiedCollection, error) { +func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (bool, map[string]StepResult, error) { vo := &verifyOptions{ searchDepth: 3, } @@ -195,16 +195,16 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][] } if err := checkVerifyOpts(vo); err != nil { - return nil, err + return false, nil, err } if time.Now().After(p.Expires.Time) { - return nil, ErrPolicyExpired(p.Expires.Time) + return false, nil, ErrPolicyExpired(p.Expires.Time) } trustBundles, err := p.TrustBundles() if err != nil { - return nil, err + return false, nil, err } attestationsByStep := make(map[string][]string) @@ -214,18 +214,36 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][] } } - passedByStep := make(map[string][]source.VerifiedCollection) + resultsByStep := make(map[string]StepResult) for depth := 0; depth < vo.searchDepth; depth++ { for stepName, step := range p.Steps { - statements, err := vo.verifiedSource.Search(ctx, stepName, vo.subjectDigests, attestationsByStep[stepName]) + // Use search to get all the attestations that match the supplied step name and subjects + collections, err := vo.verifiedSource.Search(ctx, stepName, vo.subjectDigests, attestationsByStep[stepName]) if err != nil { - return nil, err + return false, nil, err + } + + if len(collections) == 0 { + collections = append(collections, source.CollectionVerificationResult{Errors: []error{ErrNoCollections{Step: stepName}}}) + } + + // Verify the functionaries + collections = step.checkFunctionaries(collections, trustBundles) + + stepResult := step.validateAttestations(collections) + + // We perform many searches against the same step, so we need to merge the relevant fields + if resultsByStep[stepName].Step == "" { + resultsByStep[stepName] = stepResult + } else { + if result, ok := resultsByStep[stepName]; ok { + result.Passed = append(result.Passed, stepResult.Passed...) + result.Rejected = append(result.Rejected, stepResult.Rejected...) + resultsByStep[stepName] = result + } } - approvedCollections := step.checkFunctionaries(statements, trustBundles) - stepResults := step.validateAttestations(approvedCollections) - passedByStep[stepName] = append(passedByStep[stepName], stepResults.Passed...) - for _, coll := range stepResults.Passed { + for _, coll := range stepResult.Passed { for _, digestSet := range coll.Collection.BackRefs() { for _, digest := range digestSet { vo.subjectDigests = append(vo.subjectDigests, digest) @@ -233,67 +251,104 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][] } } } + } + + resultsByStep, err = p.verifyArtifacts(resultsByStep) + if err != nil { + return false, nil, fmt.Errorf("failed to verify artifacts: %w", err) + } - if accepted, err := p.verifyArtifacts(passedByStep); err == nil { - return accepted, nil + pass := true + for _, result := range resultsByStep { + p := result.Analyze() + if !p { + pass = false } } - return nil, ErrPolicyDenied{Reasons: []string{"failed to find set of attestations that satisfies the policy"}} + return pass, resultsByStep, nil } // checkFunctionaries checks to make sure the signature on each statement corresponds to a trusted functionary for // the step the statement corresponds to -func (step Step) checkFunctionaries(verifiedStatements []source.VerifiedCollection, trustBundles map[string]TrustBundle) []source.VerifiedCollection { - collections := make([]source.VerifiedCollection, 0) - for _, verifiedStatement := range verifiedStatements { - if verifiedStatement.Statement.PredicateType != attestation.CollectionType { - log.Debugf("(policy) skipping statement: predicate type is not a collection (%v)", verifiedStatement.Statement.PredicateType) - continue +func (step Step) checkFunctionaries(statements []source.CollectionVerificationResult, trustBundles map[string]TrustBundle) []source.CollectionVerificationResult { + for i, statement := range statements { + // Check that the statement contains a predicate type that we accept + if statement.Statement.PredicateType != attestation.CollectionType { + statements[i].Errors = append(statement.Errors, fmt.Errorf("predicate type %v is not a collection predicate type", statement.Statement.PredicateType)) } - for _, verifier := range verifiedStatement.Verifiers { - for _, functionary := range step.Functionaries { - if err := functionary.Validate(verifier, trustBundles); err != nil { - log.Debugf("(policy) skipping verifier: %w", err) - continue - } else { - collections = append(collections, verifiedStatement) + if len(statement.Verifiers) > 0 { + for _, verifier := range statement.Verifiers { + for _, functionary := range step.Functionaries { + if err := functionary.Validate(verifier, trustBundles); err != nil { + statements[i].Warnings = append(statement.Warnings, fmt.Sprintf("failed to validate functionary of KeyID %s in step %s: %s", functionary.PublicKeyID, step.Name, err.Error())) + continue + } else { + statements[i].ValidFunctionaries = append(statement.ValidFunctionaries, verifier) + } } } + } else { + statements[i].Errors = append(statement.Errors, fmt.Errorf("no verifiers present to validate against collection verifiers")) } } - return collections + return statements } // verifyArtifacts will check the artifacts (materials+products) of the step referred to by `ArtifactsFrom` against the // materials of the original step. This ensures file integrity between each step. -func (p Policy) verifyArtifacts(collectionsByStep map[string][]source.VerifiedCollection) (map[string][]source.VerifiedCollection, error) { - acceptedByStep := make(map[string][]source.VerifiedCollection) +func (p Policy) verifyArtifacts(resultsByStep map[string]StepResult) (map[string]StepResult, error) { for _, step := range p.Steps { - accepted := make([]source.VerifiedCollection, 0) - for _, collection := range collectionsByStep[step.Name] { - if err := verifyCollectionArtifacts(step, collection, collectionsByStep); err == nil { - accepted = append(accepted, collection) + accepted := false + if len(resultsByStep[step.Name].Passed) == 0 { + if result, ok := resultsByStep[step.Name]; ok { + result.Rejected = append(result.Rejected, RejectedCollection{Reason: fmt.Errorf("failed to verify artifacts for step %s: no passed collections present", step.Name)}) + resultsByStep[step.Name] = result + } else { + return nil, fmt.Errorf("failed to find step %s in step results map", step.Name) } + + continue } - acceptedByStep[step.Name] = accepted - if len(accepted) <= 0 { - return nil, ErrNoAttestations(step.Name) + reasons := []error{} + for _, collection := range resultsByStep[step.Name].Passed { + if err := verifyCollectionArtifacts(step, collection, resultsByStep); err == nil { + accepted = true + } else { + reasons = append(reasons, err) + } } + + if !accepted { + // can't address the map fields directly so have to make a copy and overwrite + if result, ok := resultsByStep[step.Name]; ok { + reject := RejectedCollection{Reason: fmt.Errorf("failed to verify artifacts for step %s: ", step.Name)} + for _, reason := range reasons { + reject.Reason = errors.Join(reject.Reason, reason) + } + + result.Rejected = append(result.Rejected, reject) + resultsByStep[step.Name] = result + } + } + } - return acceptedByStep, nil + return resultsByStep, nil } -func verifyCollectionArtifacts(step Step, collection source.VerifiedCollection, collectionsByStep map[string][]source.VerifiedCollection) error { +func verifyCollectionArtifacts(step Step, collection source.CollectionVerificationResult, collectionsByStep map[string]StepResult) error { mats := collection.Collection.Materials() + reasons := []string{} for _, artifactsFrom := range step.ArtifactsFrom { - accepted := make([]source.VerifiedCollection, 0) - for _, testCollection := range collectionsByStep[artifactsFrom] { + accepted := make([]source.CollectionVerificationResult, 0) + for _, testCollection := range collectionsByStep[artifactsFrom].Passed { if err := compareArtifacts(mats, testCollection.Collection.Artifacts()); err != nil { + collection.Warnings = append(collection.Warnings, fmt.Sprintf("failed to verify artifacts for step %s: %v", step.Name, err)) + reasons = append(reasons, err.Error()) break } @@ -301,7 +356,7 @@ func verifyCollectionArtifacts(step Step, collection source.VerifiedCollection, } if len(accepted) <= 0 { - return ErrNoAttestations(step.Name) + return ErrVerifyArtifactsFailed{Reasons: reasons} } } diff --git a/policy/policy_test.go b/policy/policy_test.go index 233f071c..53e0a34d 100644 --- a/policy/policy_test.go +++ b/policy/policy_test.go @@ -22,6 +22,8 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "fmt" + "log" "testing" "time" @@ -147,11 +149,11 @@ deny[msg] { intotoStatement, err := intoto.NewStatement(attestation.CollectionType, step1CollectionJson, map[string]cryptoutil.DigestSet{"dummy": {cryptoutil.DigestValue{Hash: crypto.SHA256}: "dummy"}}) require.NoError(t, err) - _, err = policy.Verify( + pass, _, err := policy.Verify( context.Background(), WithSubjectDigests([]string{"dummy"}), WithVerifiedSource( - newDummyVerifiedSourcer([]source.VerifiedCollection{ + newDummyVerifiedSourcer([]source.CollectionVerificationResult{ { Verifiers: []cryptoutil.Verifier{verifier}, CollectionEnvelope: source.CollectionEnvelope{ @@ -164,12 +166,13 @@ deny[msg] { ), ) assert.NoError(t, err) + assert.Equal(t, true, pass) - _, err = policy.Verify( + pass, results, err := policy.Verify( context.Background(), WithSubjectDigests([]string{"dummy"}), WithVerifiedSource( - newDummyVerifiedSourcer([]source.VerifiedCollection{ + newDummyVerifiedSourcer([]source.CollectionVerificationResult{ { Verifiers: []cryptoutil.Verifier{}, CollectionEnvelope: source.CollectionEnvelope{ @@ -181,8 +184,16 @@ deny[msg] { }), ), ) - assert.Error(t, err) - assert.IsType(t, ErrPolicyDenied{}, err) + assert.NoError(t, err) + assert.Equal(t, false, pass) + + for _, result := range results { + if result.Analyze() == false { + return + } + } + + assert.Fail(t, "expected a failure") } func TestArtifacts(t *testing.T) { @@ -263,10 +274,10 @@ func TestArtifacts(t *testing.T) { require.NoError(t, err) intotoStatement2, err := intoto.NewStatement(attestation.CollectionType, step2CollectionJson, map[string]cryptoutil.DigestSet{}) require.NoError(t, err) - _, err = policy.Verify( + pass, _, err := policy.Verify( context.Background(), WithSubjectDigests([]string{dummySha}), - WithVerifiedSource(newDummyVerifiedSourcer([]source.VerifiedCollection{ + WithVerifiedSource(newDummyVerifiedSourcer([]source.CollectionVerificationResult{ { Verifiers: []cryptoutil.Verifier{verifier}, CollectionEnvelope: source.CollectionEnvelope{ @@ -286,6 +297,7 @@ func TestArtifacts(t *testing.T) { })), ) assert.NoError(t, err) + assert.Equal(t, true, pass) mats[path][cryptoutil.DigestValue{Hash: crypto.SHA256}] = "badhash" @@ -302,10 +314,10 @@ func TestArtifacts(t *testing.T) { require.NoError(t, err) intotoStatement2, err = intoto.NewStatement(attestation.CollectionType, step2CollectionJson, map[string]cryptoutil.DigestSet{}) require.NoError(t, err) - _, err = policy.Verify( + pass, results, err := policy.Verify( context.Background(), WithSubjectDigests([]string{dummySha}), - WithVerifiedSource(newDummyVerifiedSourcer([]source.VerifiedCollection{ + WithVerifiedSource(newDummyVerifiedSourcer([]source.CollectionVerificationResult{ { Verifiers: []cryptoutil.Verifier{verifier}, CollectionEnvelope: source.CollectionEnvelope{ @@ -324,8 +336,19 @@ func TestArtifacts(t *testing.T) { }, })), ) - assert.Error(t, err) - assert.IsType(t, ErrPolicyDenied{}, err) + + assert.Equal(t, pass, false) + assert.NoError(t, err) + + for _, result := range results { + if result.Analyze() == false { + assert.Contains(t, result.Error(), "failed to verify artifacts for step step2") + assert.Contains(t, result.Error(), "failed to verify artifacts: [mismatched digests for testfile]") + return + } + } + + assert.Fail(t, "expected a failure") } type DummyMaterialer struct { @@ -377,14 +400,14 @@ func (m DummyProducer) Products() map[string]attestation.Product { } type dummyVerifiedSourcer struct { - verifiedCollections []source.VerifiedCollection + verifiedCollections []source.CollectionVerificationResult } -func newDummyVerifiedSourcer(verifiedCollections []source.VerifiedCollection) *dummyVerifiedSourcer { +func newDummyVerifiedSourcer(verifiedCollections []source.CollectionVerificationResult) *dummyVerifiedSourcer { return &dummyVerifiedSourcer{verifiedCollections} } -func (s *dummyVerifiedSourcer) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]source.VerifiedCollection, error) { +func (s *dummyVerifiedSourcer) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]source.CollectionVerificationResult, error) { return s.verifiedCollections, nil } @@ -462,3 +485,108 @@ func TestPubKeyVerifiers(t *testing.T) { }) } } + +func TestCheckFunctionaries(t *testing.T) { + signers := []cryptoutil.Signer{} + verifiers := []cryptoutil.Verifier{} + for i := 0; i < 7; i++ { + signer, verifier, _, err := createTestKey() + if err != nil { + log.Fatal(err) + } + + signers = append(signers, signer) + verifiers = append(verifiers, verifier) + } + + keyIDs := make([]string, 0, len(signers)) + for _, s := range signers { + keyID, err := s.KeyID() + if err != nil { + log.Fatal(err) + } + + keyIDs = append(keyIDs, keyID) + } + + testCases := []struct { + name string + step Step + statements []source.CollectionVerificationResult + trustBundles map[string]TrustBundle + // expectedResults is a list of results with each entry containing only the fields that we wish to check (errors, warnings, valid functionaries) + // this is so we can compare the results without needing to copy the unnecessary fields in the testcase definitions below + expectedResults []source.CollectionVerificationResult + }{ + { + name: "simple 1 functionary pass", + step: Step{ + Name: "step1", + Functionaries: []Functionary{ + {Type: "PublicKey", PublicKeyID: keyIDs[0]}, + }, + Attestations: []Attestation{ + {Type: "dummy-prods"}, + {Type: "dummy-mats"}, + }, + }, + statements: []source.CollectionVerificationResult{ + { + Verifiers: []cryptoutil.Verifier{verifiers[0]}, + CollectionEnvelope: source.CollectionEnvelope{ + Statement: intoto.Statement{PredicateType: attestation.CollectionType}, + }, + }, + }, + expectedResults: []source.CollectionVerificationResult{ + { + ValidFunctionaries: []cryptoutil.Verifier{ + verifiers[0], + }, + }, + }, + }, + { + name: "invalid functionary", + step: Step{ + Name: "step1", + Functionaries: []Functionary{ + {Type: "PublicKey", PublicKeyID: keyIDs[0]}, + }, + Attestations: []Attestation{ + {Type: "dummy-prods"}, + {Type: "dummy-mats"}, + }, + }, + statements: []source.CollectionVerificationResult{ + { + Verifiers: []cryptoutil.Verifier{verifiers[1]}, + CollectionEnvelope: source.CollectionEnvelope{ + Statement: intoto.Statement{PredicateType: attestation.CollectionType}, + }, + }, + }, + expectedResults: []source.CollectionVerificationResult{ + { + Warnings: []string{fmt.Sprintf("failed to validate functionary of KeyID %s in step step1: verifier with ID %s is not a public key verifier or a x509 verifier", keyIDs[0], keyIDs[1])}, + }, + }, + }, + } + + for _, testCase := range testCases { + fmt.Println("running test case: ", testCase.name) + result := testCase.step.checkFunctionaries(testCase.statements, testCase.trustBundles) + resultCheckFields := []source.CollectionVerificationResult{} + for _, r := range result { + o := source.CollectionVerificationResult{ + Errors: r.Errors, + Warnings: r.Warnings, + ValidFunctionaries: r.ValidFunctionaries, + } + resultCheckFields = append(resultCheckFields, o) + } + + assert.Equal(t, testCase.expectedResults, resultCheckFields) + } +} diff --git a/policy/rego.go b/policy/rego.go index 51e71e81..b5c1219c 100644 --- a/policy/rego.go +++ b/policy/rego.go @@ -89,7 +89,7 @@ func EvaluateRegoPolicy(attestor attestation.Attestor, policies []RegoPolicy) er } if len(allDenyReasons) > 0 { - return ErrPolicyDenied{Reasons: allDenyReasons} + return fmt.Errorf("rego policy evaluation failed for attestor type %s: %w", attestor.Type(), ErrPolicyDenied{Reasons: allDenyReasons}) } return nil diff --git a/policy/rego_test.go b/policy/rego_test.go index ab767e9e..0c9a5f37 100644 --- a/policy/rego_test.go +++ b/policy/rego_test.go @@ -44,8 +44,7 @@ deny[msg]{ attestor.Status["test"] = git.Status{Staging: "Modified"} err := EvaluateRegoPolicy(&attestor, passPolicy) assert.Error(t, err) - require.IsType(t, ErrPolicyDenied{}, err) - assert.ElementsMatch(t, []string{expectedReason}, err.(ErrPolicyDenied).Reasons) + require.Contains(t, err.Error(), "unexpected changes to git repository") } func TestInvalidDeny(t *testing.T) { diff --git a/policy/step.go b/policy/step.go index ea451bf8..7f121e6f 100644 --- a/policy/step.go +++ b/policy/step.go @@ -20,6 +20,7 @@ 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/source" ) @@ -55,10 +56,39 @@ type RegoPolicy struct { // Rejected contains the rejected collections and the error that caused them to be rejected. type StepResult struct { Step string - Passed []source.VerifiedCollection + Passed []source.CollectionVerificationResult Rejected []RejectedCollection } +// Analyze inspects the StepResult to determine if the step passed or failed. +// We do this rather than failing at the first point of failure in the verification flow +// in order to save the failure reasons so we can present them all at the end of the verification process. +func (r StepResult) Analyze() bool { + var pass bool + if len(r.Passed) > 0 && len(r.Rejected) == 0 { + pass = true + } + + for _, coll := range r.Passed { + // we don't fail on warnings so we process these under debug logs + if len(coll.Warnings) > 0 { + for _, warn := range coll.Warnings { + log.Debug("Warning: Step: %s, Collection: %s, Warning: %s", r.Step, coll.Collection.Name, warn) + } + } + + // Want to ensure that undiscovered errors aren't lurking in the passed collections + if len(coll.Errors) > 0 { + for _, err := range coll.Errors { + pass = false + log.Errorf("Unexpected Error in Passed Collection: Step: %s, Collection: %s, Error: %s", r.Step, coll.Collection.Name, err) + } + } + } + + return pass +} + func (r StepResult) HasErrors() bool { return len(r.Rejected) > 0 } @@ -77,7 +107,7 @@ func (r StepResult) Error() string { } type RejectedCollection struct { - Collection source.VerifiedCollection + Collection source.CollectionVerificationResult Reason error } @@ -109,47 +139,57 @@ func (f Functionary) Validate(verifier cryptoutil.Verifier, trustBundles map[str // validateAttestations will test each collection against to ensure the expected attestations // appear in the collection as well as that any rego policies pass for the step. -func (s Step) validateAttestations(verifiedCollections []source.VerifiedCollection) StepResult { +func (s Step) validateAttestations(collectionResults []source.CollectionVerificationResult) StepResult { result := StepResult{Step: s.Name} - if len(verifiedCollections) <= 0 { + if len(collectionResults) <= 0 { return result } - for _, collection := range verifiedCollections { + for _, collection := range collectionResults { + if collection.Collection.Name != s.Name && collection.Collection.Name != "" { + log.Debugf("Skipping collection %s as it is not for step %s", collection.Collection.Name, s.Name) + continue + } + found := make(map[string]attestation.Attestor) + reasons := make([]string, 0) + passed := true + if len(collection.Errors) > 0 { + passed = false + for _, err := range collection.Errors { + reasons = append(reasons, fmt.Sprintf("collection verification failed: %s", err.Error())) + } + } + for _, attestation := range collection.Collection.Attestations { found[attestation.Type] = attestation.Attestation } - passed := true for _, expected := range s.Attestations { attestor, ok := found[expected.Type] if !ok { - result.Rejected = append(result.Rejected, RejectedCollection{ - Collection: collection, - Reason: ErrMissingAttestation{ - Step: s.Name, - Attestation: expected.Type, - }, - }) - passed = false - break + reasons = append(reasons, ErrMissingAttestation{ + Step: s.Name, + Attestation: expected.Type, + }.Error()) } if err := EvaluateRegoPolicy(attestor, expected.RegoPolicies); err != nil { - result.Rejected = append(result.Rejected, RejectedCollection{ - Collection: collection, - Reason: err, - }) - passed = false - break + reasons = append(reasons, err.Error()) } } if passed { result.Passed = append(result.Passed, collection) + } else { + r := strings.Join(reasons, ",\n - ") + reason := fmt.Sprintf("collection validation failed:\n - %s", r) + result.Rejected = append(result.Rejected, RejectedCollection{ + Collection: collection, + Reason: fmt.Errorf("%s", reason), + }) } } diff --git a/signer/kms/gcp/fakeclient.go b/signer/kms/gcp/fakeclient.go index cf0c9457..89c57981 100644 --- a/signer/kms/gcp/fakeclient.go +++ b/signer/kms/gcp/fakeclient.go @@ -64,7 +64,6 @@ type fakeGCPClient struct { } func newFakeGCPClient(ctx context.Context, ksp *kms.KMSSignerProvider) (*fakeGCPClient, error) { - fmt.Println(ksp.Reference) if err := ValidReference(ksp.Reference); err != nil { return nil, err } diff --git a/source/verified.go b/source/verified.go index 46fc561e..fab6404b 100644 --- a/source/verified.go +++ b/source/verified.go @@ -16,19 +16,22 @@ package source import ( "context" + "fmt" "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/dsse" - "github.com/in-toto/go-witness/log" ) -type VerifiedCollection struct { - Verifiers []cryptoutil.Verifier +type CollectionVerificationResult struct { + Verifiers []cryptoutil.Verifier + ValidFunctionaries []cryptoutil.Verifier CollectionEnvelope + Errors []error + Warnings []string } type VerifiedSourcer interface { - Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]VerifiedCollection, error) + Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]CollectionVerificationResult, error) } type VerifiedSource struct { @@ -40,17 +43,22 @@ func NewVerifiedSource(source Sourcer, verifyOpts ...dsse.VerificationOption) *V return &VerifiedSource{source, verifyOpts} } -func (s *VerifiedSource) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]VerifiedCollection, error) { +func (s *VerifiedSource) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]CollectionVerificationResult, error) { unverified, err := s.source.Search(ctx, collectionName, subjectDigests, attestations) if err != nil { return nil, err } - verified := make([]VerifiedCollection, 0) + results := make([]CollectionVerificationResult, 0) for _, toVerify := range unverified { envelopeVerifiers, err := toVerify.Envelope.Verify(s.verifyOpts...) if err != nil { - log.Debugf("(verified source) skipping envelope: couldn't verify enveloper's signature with the policy's verifiers: %w", err) + results = append(results, + CollectionVerificationResult{ + Errors: []error{fmt.Errorf("failed to verify envelope: %w", err)}, + CollectionEnvelope: toVerify, + }, + ) continue } @@ -59,11 +67,17 @@ func (s *VerifiedSource) Search(ctx context.Context, collectionName string, subj passedVerifiers = append(passedVerifiers, verifier.Verifier) } - verified = append(verified, VerifiedCollection{ + var Errors []error + if len(passedVerifiers) == 0 { + Errors = append(Errors, fmt.Errorf("no verifiers passed")) + } + + results = append(results, CollectionVerificationResult{ Verifiers: passedVerifiers, CollectionEnvelope: toVerify, + Errors: Errors, }) } - return verified, nil + return results, nil } diff --git a/verify.go b/verify.go index 3ff44fb8..3ba384e8 100644 --- a/verify.go +++ b/verify.go @@ -104,7 +104,7 @@ func VerifyWithPolicyCertConstraints(commonName string, dnsNames []string, email // 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) { +func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers []cryptoutil.Verifier, opts ...VerifyOption) (map[string]policy.StepResult, error) { vo := verifyOptions{ policyEnvelope: policyEnvelope, policyVerifiers: policyVerifiers, @@ -127,7 +127,7 @@ func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers [ pol := policy.Policy{} if err := json.Unmarshal(vo.policyEnvelope.Payload, &pol); err != nil { - return nil, fmt.Errorf("failed to unmarshal policy from envelope: %w", err) + return nil, fmt.Errorf("failed to parse policy: %w", err) } pubKeysById, err := pol.PublicKeyVerifiers() @@ -171,12 +171,17 @@ func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers [ dsse.VerifyWithIntermediates(intermediates...), dsse.VerifyWithTimestampVerifiers(timestampVerifiers...), ) - accepted, err := pol.Verify(ctx, policy.WithSubjectDigests(vo.subjectDigests), policy.WithVerifiedSource(verifiedSource)) + + pass, results, err := pol.Verify(ctx, policy.WithSubjectDigests(vo.subjectDigests), policy.WithVerifiedSource(verifiedSource)) if err != nil { - return nil, fmt.Errorf("failed to verify policy: %w", err) + return nil, fmt.Errorf("error encountered during policy verification: %w", err) + } + + if !pass { + return results, fmt.Errorf("policy verification failed") } - return accepted, nil + return results, nil } func verifyPolicySignature(ctx context.Context, vo verifyOptions) error { diff --git a/verify_test.go b/verify_test.go index cfb32d5d..adfd101c 100644 --- a/verify_test.go +++ b/verify_test.go @@ -22,11 +22,12 @@ import ( "testing" "time" - "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/dsse" "github.com/in-toto/go-witness/internal/test" "github.com/in-toto/go-witness/intoto" "github.com/in-toto/go-witness/timestamp" + + "github.com/in-toto/go-witness/cryptoutil" ) func TestVerifyPolicySignature(t *testing.T) {