From 20506224f71cd45669deb351ac2f1a6ec4964e50 Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Sat, 4 Nov 2023 00:15:32 -0400 Subject: [PATCH] wip: more hacking for demo -- add reasons attestations were rejected to VSA --- attestation/policyverify/policyverify.go | 106 ++++++++++++++++------- policy/policy.go | 92 +++++++++++++++----- policy/step.go | 21 +++-- 3 files changed, 158 insertions(+), 61 deletions(-) diff --git a/attestation/policyverify/policyverify.go b/attestation/policyverify/policyverify.go index bf0c47f2..b519c6cc 100644 --- a/attestation/policyverify/policyverify.go +++ b/attestation/policyverify/policyverify.go @@ -58,6 +58,13 @@ type WitnessVerifyInfo struct { InitialSubjectDigests []cryptoutil.DigestSet `json:"initialsubjectdigests,omitempty"` // AdditionalSubjects is a set of subjects that were used during the verification process. AdditionalSubjects map[string]cryptoutil.DigestSet `json:"additionalsubjects,omitempty"` + // RejectedAttestations is a list of the attestations that were rejected by our policy and a reason why + RejectedAttestations []RejectedAttestation `json:"rejectedattestations,omitempty"` +} + +type RejectedAttestation struct { + slsa.ResourceDescriptor `json:"resourcedescriptor"` + ReasonRejected string `json:"reasonrejected"` } type Option func(*Attestor) @@ -179,20 +186,18 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { dsse.VerifyWithTimestampVerifiers(timestampVerifiers...), ) - accepted := true - policyResult, policyErr := pol.Verify(ctx.Context(), policy.WithSubjectDigests(a.WitnessVerifyInfo.InitialSubjectDigests), policy.WithVerifiedSource(verifiedSource)) - if _, ok := policyErr.(policy.ErrPolicyDenied); ok { - accepted = false - } else if policyErr != nil { + policyResult, err := pol.Verify(ctx.Context(), policy.WithSubjectDigests(a.WitnessVerifyInfo.InitialSubjectDigests), policy.WithVerifiedSource(verifiedSource)) + if err != nil { return fmt.Errorf("failed to verify policy: %w", err) } - a.VerificationSummary, err = verificationSummaryFromResults(ctx, a.policyEnvelope, policyResult, accepted) + a.VerificationSummary, err = verificationSummaryFromResults(ctx, a.policyEnvelope, policyResult) if err != nil { return fmt.Errorf("failed to generate verification summary: %w", err) } - a.findInterestingSubjects(policyResult.EvidenceByStep) + a.WitnessVerifyInfo.AdditionalSubjects = findInterestingSubjects(policyResult, a.WitnessVerifyInfo.InitialSubjectDigests) + a.WitnessVerifyInfo.RejectedAttestations = rejectedAttestations(ctx, policyResult) return nil } @@ -200,7 +205,7 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { // for interesting subjects, and package them onto the VSA as additional subjects. This is used // primarily to link a VSA back to a specific github or gitlab project, or an artifact hash to // a specific tagged image. -func (a *Attestor) findInterestingSubjects(evidenceByStep map[string][]source.VerifiedCollection) { +func findInterestingSubjects(policyResult policy.PolicyResult, initialSubjectDigests []cryptoutil.DigestSet) map[string]cryptoutil.DigestSet { // imageId is especially interesting, and we only treat the other interesting subject candidates // as valid if we get a match on the imageId const imageIdSubjectPrefix = "https://witness.dev/attestations/oci/v0.1/imageid:" @@ -214,8 +219,19 @@ func (a *Attestor) findInterestingSubjects(evidenceByStep map[string][]source.Ve "https://witness.dev/attestations/git/v0.1/commithash:": "commithash", } - for _, collections := range evidenceByStep { - for _, collection := range collections { + foundSubjects := make(map[string]cryptoutil.DigestSet) + for _, stepResults := range policyResult.ResultsByStep { + // if our policy passed, we only care about interesting subjects on attestations that satisfied our policy. + // if it didn't, though, we want information off the failed ones. + collectionsToSearch := stepResults.Passed + if !policyResult.Passed { + collectionsToSearch = make([]source.VerifiedCollection, 0) + for _, rejects := range stepResults.Rejected { + collectionsToSearch = append(collectionsToSearch, rejects.Collection) + } + } + + for _, collection := range collectionsToSearch { candidates := make([]intoto.Subject, 0) matchedSubject := false @@ -234,7 +250,7 @@ func (a *Attestor) findInterestingSubjects(evidenceByStep map[string][]source.Ve // if we find an imageid subject, check to see if any the digests we verified match the imageid if strings.HasPrefix(subject.Name, imageIdSubjectPrefix) { for _, imageIdDigest := range subject.Digest { - for _, testDigestSet := range a.WitnessVerifyInfo.InitialSubjectDigests { + for _, testDigestSet := range initialSubjectDigests { for _, testImageIdDigest := range testDigestSet { if imageIdDigest == testImageIdDigest { matchedSubject = true @@ -267,38 +283,25 @@ func (a *Attestor) findInterestingSubjects(evidenceByStep map[string][]source.Ve ds[digestValue] = value } - a.WitnessVerifyInfo.AdditionalSubjects[candidate.Name] = ds + foundSubjects[candidate.Name] = ds } } } } } -} - -func verificationSummaryFromResults(ctx *attestation.AttestationContext, policyEnvelope dsse.Envelope, policyResult policy.PolicyResult, accepted bool) (slsa.VerificationSummary, error) { - inputAttestations := make([]slsa.ResourceDescriptor, 0, len(policyResult.EvidenceByStep)) - for _, input := range policyResult.EvidenceByStep { - for _, attestation := range input { - digest, err := cryptoutil.CalculateDigestSetFromBytes(attestation.Envelope.Payload, ctx.Hashes()) - if err != nil { - log.Debugf("failed to calculate evidence hash: %v", err) - continue - } - inputAttestations = append(inputAttestations, slsa.ResourceDescriptor{ - URI: attestation.Reference, - Digest: digest, - }) - } - } + return foundSubjects +} +func verificationSummaryFromResults(ctx *attestation.AttestationContext, policyEnvelope dsse.Envelope, policyResult policy.PolicyResult) (slsa.VerificationSummary, error) { + inputAttestations := inputAttestationsFromResults(ctx, policyResult) policyDigest, err := cryptoutil.CalculateDigestSetFromBytes(policyEnvelope.Payload, ctx.Hashes()) if err != nil { return slsa.VerificationSummary{}, fmt.Errorf("failed to calculate policy digest: %w", err) } verificationResult := slsa.FailedVerificationResult - if accepted { + if policyResult.Passed { verificationResult = slsa.PassedVerificationResult } @@ -315,3 +318,46 @@ func verificationSummaryFromResults(ctx *attestation.AttestationContext, policyE VerificationResult: verificationResult, }, nil } + +func inputAttestationsFromResults(ctx *attestation.AttestationContext, policyResult policy.PolicyResult) []slsa.ResourceDescriptor { + inputAttestations := make([]slsa.ResourceDescriptor, 0) + for _, input := range policyResult.ResultsByStep { + // if our policy passed we'll only record our passed attestations as input attestations. if it didn't pass, we'll record every attestation we tried to use. + if policyResult.Passed { + for _, coll := range input.Passed { + inputAttestations = append(inputAttestations, slsaResourceDescriptorFromAttestation(ctx, coll)) + } + } else { + for _, reject := range input.Rejected { + inputAttestations = append(inputAttestations, slsaResourceDescriptorFromAttestation(ctx, reject.Collection)) + } + } + } + + return inputAttestations +} + +func slsaResourceDescriptorFromAttestation(ctx *attestation.AttestationContext, attestation source.VerifiedCollection) slsa.ResourceDescriptor { + digest, err := cryptoutil.CalculateDigestSetFromBytes(attestation.Envelope.Payload, ctx.Hashes()) + if err != nil { + log.Debugf("failed to calculate evidence hash: %v", err) + } + + return slsa.ResourceDescriptor{ + URI: attestation.Reference, + Digest: digest, + } +} + +func rejectedAttestations(ctx *attestation.AttestationContext, policyResults policy.PolicyResult) []RejectedAttestation { + rejecetedAttestations := make([]RejectedAttestation, 0) + for _, stepResult := range policyResults.ResultsByStep { + for _, reject := range stepResult.Rejected { + rejecetedAttestations = append(rejecetedAttestations, RejectedAttestation{ + ResourceDescriptor: slsaResourceDescriptorFromAttestation(ctx, reject.Collection), + ReasonRejected: reject.Reason.Error(), + }) + } + } + return rejecetedAttestations +} diff --git a/policy/policy.go b/policy/policy.go index 8f752c1d..306fc3af 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "crypto/x509" + "fmt" "time" "github.com/testifysec/go-witness/attestation" @@ -169,7 +170,8 @@ func checkVerifyOpts(vo *verifyOptions) error { } type PolicyResult struct { - EvidenceByStep map[string][]source.VerifiedCollection + Passed bool + ResultsByStep map[string]StepResult } func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (PolicyResult, error) { @@ -201,7 +203,7 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (PolicyResult, } } - 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]) @@ -209,30 +211,59 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (PolicyResult, return PolicyResult{}, err } - approvedCollections := step.checkFunctionaries(statements, trustBundles) - stepResults := step.validateAttestations(approvedCollections) - passedByStep[stepName] = append(passedByStep[stepName], stepResults.Passed...) + stepResults := step.checkFunctionaries(statements, trustBundles) + stepResults = step.validateAttestations(stepResults) + + // go through our new pass results and add any found backrefs to our search digests for _, coll := range stepResults.Passed { for _, digestSet := range coll.Collection.BackRefs() { vo.subjectDigests = append(vo.subjectDigests, digestSet) } } + + // merge the existing passed results with our new ones + oldResults := resultsByStep[stepName] + oldResults.Passed = append(oldResults.Passed, stepResults.Passed...) + oldResults.Rejected = append(oldResults.Rejected, stepResults.Rejected...) + resultsByStep[stepName] = oldResults + } + + if _, err := p.verifyArtifacts(resultsByStep); err == nil { + return PolicyResult{ResultsByStep: resultsByStep, Passed: true}, nil } + } - if accepted, err := p.verifyArtifacts(passedByStep); err == nil { - return PolicyResult{EvidenceByStep: accepted}, nil + // mark all currently marked passed results as failed + for step, results := range resultsByStep { + modifiedResults := results + for _, passed := range results.Passed { + modifiedResults.Rejected = append(modifiedResults.Rejected, RejectedCollection{ + Collection: passed, + Reason: err, + }) } + + modifiedResults.Passed = nil + resultsByStep[step] = modifiedResults } - return PolicyResult{}, ErrPolicyDenied{Reasons: []string{"failed to find set of attestations that satisfies the policy"}} + return PolicyResult{Passed: false, ResultsByStep: resultsByStep}, ErrPolicyDenied{Reasons: []string{"failed to find set of attestations that satisfies the policy"}} } // 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) +func (step Step) checkFunctionaries(verifiedStatements []source.VerifiedCollection, trustBundles map[string]TrustBundle) StepResult { + stepResult := StepResult{ + Step: step.Name, + } + for _, verifiedStatement := range verifiedStatements { if verifiedStatement.Statement.PredicateType != attestation.CollectionType { + stepResult.Rejected = append(stepResult.Rejected, RejectedCollection{ + Collection: verifiedStatement, + Reason: fmt.Errorf("unrecognized predicate: %v", verifiedStatement.Statement.PredicateType), + }) + log.Debugf("(policy) skipping statement: predicate type is not a collection (%v)", verifiedStatement.Statement.PredicateType) continue } @@ -244,9 +275,10 @@ func (step Step) checkFunctionaries(verifiedStatements []source.VerifiedCollecti continue } + passedFunctionary := false for _, functionary := range step.Functionaries { if functionary.PublicKeyID != "" && functionary.PublicKeyID == verifierID { - collections = append(collections, verifiedStatement) + passedFunctionary = true break } @@ -264,42 +296,56 @@ func (step Step) checkFunctionaries(verifiedStatements []source.VerifiedCollecti if err := functionary.CertConstraint.Check(x509Verifier, trustBundles); err != nil { log.Debugf("(policy) skipping verifier: verifier with ID %v doesn't meet certificate constraint: %w", verifierID, err) continue + } else { + passedFunctionary = true } + } - collections = append(collections, verifiedStatement) + if passedFunctionary { + stepResult.Passed = append(stepResult.Passed, verifiedStatement) + } else { + stepResult.Rejected = append(stepResult.Rejected, RejectedCollection{ + Collection: verifiedStatement, + Reason: fmt.Errorf("no signature from allowed functionary"), + }) } } } - return collections + return stepResult } // 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(collectionsByStep map[string]StepResult) (map[string]StepResult, error) { for _, step := range p.Steps { - accepted := make([]source.VerifiedCollection, 0) - for _, collection := range collectionsByStep[step.Name] { + newResultsByStep := collectionsByStep[step.Name] + newResultsByStep.Passed = make([]source.VerifiedCollection, 0) + for _, collection := range collectionsByStep[step.Name].Passed { if err := verifyCollectionArtifacts(step, collection, collectionsByStep); err == nil { - accepted = append(accepted, collection) + newResultsByStep.Passed = append(newResultsByStep.Passed, collection) + } else { + newResultsByStep.Rejected = append(newResultsByStep.Rejected, RejectedCollection{ + Collection: collection, + Reason: err, + }) } } - acceptedByStep[step.Name] = accepted - if len(accepted) <= 0 { + collectionsByStep[step.Name] = newResultsByStep + if len(newResultsByStep.Passed) <= 0 { return nil, ErrNoAttestations(step.Name) } } - return acceptedByStep, nil + return collectionsByStep, nil } -func verifyCollectionArtifacts(step Step, collection source.VerifiedCollection, collectionsByStep map[string][]source.VerifiedCollection) error { +func verifyCollectionArtifacts(step Step, collection source.VerifiedCollection, resultsByStep map[string]StepResult) error { mats := collection.Collection.Materials() for _, artifactsFrom := range step.ArtifactsFrom { accepted := make([]source.VerifiedCollection, 0) - for _, testCollection := range collectionsByStep[artifactsFrom] { + for _, testCollection := range resultsByStep[artifactsFrom].Passed { if err := compareArtifacts(mats, testCollection.Collection.Artifacts()); err != nil { break } diff --git a/policy/step.go b/policy/step.go index 15a54001..3a151a77 100644 --- a/policy/step.go +++ b/policy/step.go @@ -82,13 +82,18 @@ type RejectedCollection struct { // 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 { - result := StepResult{Step: s.Name} - if len(verifiedCollections) <= 0 { +func (s Step) validateAttestations(result StepResult) StepResult { + // clone the result we were given, but start with no passed collections. + // we'll iterate through the old result's passed collections and add them to either passed + // or rejected on the new result. we want to keep the existing rejected results, though. + newResult := result + newResult.Passed = []source.VerifiedCollection{} + + if len(result.Passed) <= 0 { return result } - for _, collection := range verifiedCollections { + for _, collection := range result.Passed { found := make(map[string]attestation.Attestor) for _, attestation := range collection.Collection.Attestations { found[attestation.Type] = attestation.Attestation @@ -98,7 +103,7 @@ func (s Step) validateAttestations(verifiedCollections []source.VerifiedCollecti for _, expected := range s.Attestations { attestor, ok := found[expected.Type] if !ok { - result.Rejected = append(result.Rejected, RejectedCollection{ + newResult.Rejected = append(newResult.Rejected, RejectedCollection{ Collection: collection, Reason: ErrMissingAttestation{ Step: s.Name, @@ -111,7 +116,7 @@ func (s Step) validateAttestations(verifiedCollections []source.VerifiedCollecti } if err := EvaluateRegoPolicy(attestor, expected.RegoPolicies); err != nil { - result.Rejected = append(result.Rejected, RejectedCollection{ + newResult.Rejected = append(newResult.Rejected, RejectedCollection{ Collection: collection, Reason: err, }) @@ -122,9 +127,9 @@ func (s Step) validateAttestations(verifiedCollections []source.VerifiedCollecti } if passed { - result.Passed = append(result.Passed, collection) + newResult.Passed = append(newResult.Passed, collection) } } - return result + return newResult }