Skip to content

Commit

Permalink
wip: more hacking for demo -- add reasons attestations were rejected …
Browse files Browse the repository at this point in the history
…to VSA
  • Loading branch information
mikhailswift committed Nov 4, 2023
1 parent 120ec25 commit bc07f67
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 61 deletions.
106 changes: 76 additions & 30 deletions attestation/policyverify/policyverify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -179,28 +186,26 @@ 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
}

// findInterestingSubjects will search subjects of attestations used during the verification process
// 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:"
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
92 changes: 69 additions & 23 deletions policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"bytes"
"context"
"crypto/x509"
"fmt"
"time"

"github.com/testifysec/go-witness/attestation"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -201,38 +203,67 @@ 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])
if err != nil {
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 accepted, err := p.verifyArtifacts(passedByStep); err == nil {
return PolicyResult{EvidenceByStep: accepted}, nil
if _, err := p.verifyArtifacts(resultsByStep); err == 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{ResultsByStep: resultsByStep, Passed: true}, nil
}
}

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
}
Expand All @@ -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
}

Expand All @@ -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
}
Expand Down
Loading

0 comments on commit bc07f67

Please sign in to comment.