From 120ec25839e288f98e4754fa33a8d0feea16eaac Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Fri, 3 Nov 2023 20:48:27 -0400 Subject: [PATCH] wip: more hacking -- adjust how we handle additional subjects and use digestsets in more palces --- attestation/policyverify/policyverify.go | 148 ++++++++++++----------- policy/policy.go | 8 +- policy/policy_test.go | 10 +- source/archivista.go | 11 +- source/memory.go | 13 +- source/multi.go | 8 +- source/source.go | 3 +- source/verified.go | 4 +- 8 files changed, 111 insertions(+), 94 deletions(-) diff --git a/attestation/policyverify/policyverify.go b/attestation/policyverify/policyverify.go index 77eefcfb..bf0c47f2 100644 --- a/attestation/policyverify/policyverify.go +++ b/attestation/policyverify/policyverify.go @@ -45,12 +45,19 @@ var ( type Attestor struct { slsa.VerificationSummary + WitnessVerifyInfo WitnessVerifyInfo `json:"witnessverifyinfo,omitempty"` policyEnvelope dsse.Envelope policyVerifiers []cryptoutil.Verifier collectionSource source.Sourcer - subjectDigests []string - addtlSubjects map[string]cryptoutil.DigestSet +} + +type WitnessVerifyInfo struct { + // InitialSubjectDigests is the set of subject digests passed to witness Verify to start + // the verification process + 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"` } type Option func(*Attestor) @@ -69,11 +76,7 @@ func VerifyWithPolicyVerifiers(policyVerifiers []cryptoutil.Verifier) Option { func VerifyWithSubjectDigests(subjectDigests []cryptoutil.DigestSet) Option { return func(a *Attestor) { - for _, set := range subjectDigests { - for _, digest := range set { - a.subjectDigests = append(a.subjectDigests, digest) - } - } + a.WitnessVerifyInfo.InitialSubjectDigests = append(a.WitnessVerifyInfo.InitialSubjectDigests, subjectDigests...) } } @@ -84,7 +87,13 @@ func VerifyWithCollectionSource(source source.Sourcer) Option { } func New(opts ...Option) *Attestor { - a := &Attestor{addtlSubjects: make(map[string]cryptoutil.DigestSet)} + a := &Attestor{ + WitnessVerifyInfo: WitnessVerifyInfo{ + AdditionalSubjects: make(map[string]cryptoutil.DigestSet), + InitialSubjectDigests: make([]cryptoutil.DigestSet, 0), + }, + } + for _, opt := range opts { opt(a) } @@ -106,14 +115,12 @@ func (a *Attestor) RunType() attestation.RunType { func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet { subjects := map[string]cryptoutil.DigestSet{} - for _, digest := range a.subjectDigests { - subjects[fmt.Sprintf("artifact:%v", digest)] = cryptoutil.DigestSet{ - cryptoutil.DigestValue{Hash: crypto.SHA256, GitOID: false}: digest, - } + for n, digestSet := range a.WitnessVerifyInfo.InitialSubjectDigests { + subjects[fmt.Sprintf("artifact:%v", n)] = digestSet } subjects[fmt.Sprintf("policy:%v", a.VerificationSummary.Policy.URI)] = a.VerificationSummary.Policy.Digest - for name, ds := range a.addtlSubjects { + for name, ds := range a.WitnessVerifyInfo.AdditionalSubjects { subjects[name] = ds } @@ -173,7 +180,7 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { ) accepted := true - policyResult, policyErr := pol.Verify(ctx.Context(), policy.WithSubjectDigests(a.subjectDigests), policy.WithVerifiedSource(verifiedSource)) + 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 { @@ -185,86 +192,83 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { return fmt.Errorf("failed to generate verification summary: %w", err) } - a.findRelevantSubjects(policyResult.EvidenceByStep) + a.findInterestingSubjects(policyResult.EvidenceByStep) return nil } -// findRelevantSubjects will find any image tags in attestations that passed our policy check. -// we do this by searching the subject digests the user tested the policy against, and if we find -// a collection with OCI subjects for an imageid that matches one of the provided subject digests, -// we grab the imagetag subjects off of that attestation. -// todo: we need a better solution for this -func (a *Attestor) findRelevantSubjects(evidenceByStep map[string][]source.VerifiedCollection) { - const imageTagSubjectPrefix = "https://witness.dev/attestations/oci/v0.1/imagetag:" +// 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) { + // 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:" - const githubProjectPrefix = "https://witness.dev/attestations/github/v0.1/projecturl:" - const gitlabProjectPrefix = "https://witness.dev/attestations/gitlab/v0.1/projecturl:" + + // a map of subjects we consider interesting. the value of this map is just a value we'll use + // to repackage the subject as a subject of the VSA itself. + interestingSubjects := map[string]string{ + "https://witness.dev/attestations/oci/v0.1/imagetag:": "imagetag", + "https://witness.dev/attestations/github/v0.1/projecturl:": "projecturl", + "https://witness.dev/attestations/gitlab/v0.1/projecturl:": "projecturl", + "https://witness.dev/attestations/git/v0.1/commithash:": "commithash", + } for _, collections := range evidenceByStep { for _, collection := range collections { candidates := make([]intoto.Subject, 0) matchedSubject := false - // search through every subject on the in-toto statment. if we find imagetags, we set them aside as possible candidates. - // if we find an imageid subject that matches, we consider all the candidates to be matching subjects and return them + // search through every subject on the in-toto statment. if we find any interesting subjects, we set them aside as possible candidates. + // if we find an imageid subject that matches, we consider all the candidates to be matching subjects and add them to our list for _, subject := range collection.Statement.Subject { // if we find an image tag subject, add it to the list of candidates - if strings.HasPrefix(subject.Name, imageTagSubjectPrefix) { - candidates = append(candidates, intoto.Subject{ - Name: fmt.Sprintf("imagetag:%v", strings.TrimPrefix(subject.Name, imageTagSubjectPrefix)), - Digest: subject.Digest, - }) - } - - if strings.HasPrefix(subject.Name, githubProjectPrefix) { - candidates = append(candidates, intoto.Subject{ - Name: fmt.Sprintf("projecturl:%v", strings.TrimPrefix(subject.Name, githubProjectPrefix)), - Digest: subject.Digest, - }) - } - - if strings.HasPrefix(subject.Name, gitlabProjectPrefix) { - candidates = append(candidates, intoto.Subject{ - Name: fmt.Sprintf("projecturl:%v", strings.TrimPrefix(subject.Name, gitlabProjectPrefix)), - Digest: subject.Digest, - }) - } + for interstingSubject, transformedSubject := range interestingSubjects { + if strings.HasPrefix(subject.Name, interstingSubject) { + candidates = append(candidates, intoto.Subject{ + Name: fmt.Sprintf("%v:%v", transformedSubject, strings.TrimPrefix(subject.Name, interstingSubject)), + Digest: subject.Digest, + }) + } - // 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 _, testImageIdDigest := range a.subjectDigests { - if imageIdDigest == testImageIdDigest { - matchedSubject = true - candidates = append(candidates, intoto.Subject{ - Name: fmt.Sprintf("imageid:%v", testImageIdDigest), - Digest: subject.Digest, - }) + // 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 _, testImageIdDigest := range testDigestSet { + if imageIdDigest == testImageIdDigest { + matchedSubject = true + candidates = append(candidates, intoto.Subject{ + Name: fmt.Sprintf("imageid:%v", testImageIdDigest), + Digest: subject.Digest, + }) + } + } } - } - // if we found a matching imageid subject with one of our test subject digests, stop looking - if matchedSubject { - break + // if we found a matching imageid subject with one of our test subject digests, stop looking + if matchedSubject { + break + } } } } - } - // after we've checked all the subjects, if we found a match, add our candidates to our additional subjects - if matchedSubject { - for _, candidate := range candidates { - ds := cryptoutil.DigestSet{} - for hash, value := range candidate.Digest { - digestValue, err := cryptoutil.DigestValueFromString(hash) - if err != nil { - continue + // after we've checked all the subjects, if we found a match, add our candidates to our additional subjects + if matchedSubject { + for _, candidate := range candidates { + ds := cryptoutil.DigestSet{} + for hash, value := range candidate.Digest { + digestValue, err := cryptoutil.DigestValueFromString(hash) + if err != nil { + continue + } + + ds[digestValue] = value } - ds[digestValue] = value + a.WitnessVerifyInfo.AdditionalSubjects[candidate.Name] = ds } - - a.addtlSubjects[candidate.Name] = ds } } } diff --git a/policy/policy.go b/policy/policy.go index a77b4e8c..8f752c1d 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -121,7 +121,7 @@ type VerifyOption func(*verifyOptions) type verifyOptions struct { verifiedSource source.VerifiedSourcer - subjectDigests []string + subjectDigests []cryptoutil.DigestSet searchDepth int } @@ -131,7 +131,7 @@ func WithVerifiedSource(verifiedSource source.VerifiedSourcer) VerifyOption { } } -func WithSubjectDigests(subjectDigests []string) VerifyOption { +func WithSubjectDigests(subjectDigests []cryptoutil.DigestSet) VerifyOption { return func(vo *verifyOptions) { vo.subjectDigests = subjectDigests } @@ -214,9 +214,7 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (PolicyResult, passedByStep[stepName] = append(passedByStep[stepName], stepResults.Passed...) for _, coll := range stepResults.Passed { for _, digestSet := range coll.Collection.BackRefs() { - for _, digest := range digestSet { - vo.subjectDigests = append(vo.subjectDigests, digest) - } + vo.subjectDigests = append(vo.subjectDigests, digestSet) } } } diff --git a/policy/policy_test.go b/policy/policy_test.go index 6e767a20..3794d5a1 100644 --- a/policy/policy_test.go +++ b/policy/policy_test.go @@ -146,7 +146,7 @@ deny[msg] { _, err = policy.Verify( context.Background(), - WithSubjectDigests([]string{"dummy"}), + WithSubjectDigests([]cryptoutil.DigestSet{{cryptoutil.DigestValue{Hash: crypto.SHA256}: "dummy"}}), WithVerifiedSource( newDummyVerifiedSourcer([]source.VerifiedCollection{ { @@ -164,7 +164,7 @@ deny[msg] { _, err = policy.Verify( context.Background(), - WithSubjectDigests([]string{"dummy"}), + WithSubjectDigests([]cryptoutil.DigestSet{{cryptoutil.DigestValue{Hash: crypto.SHA256}: "dummy"}}), WithVerifiedSource( newDummyVerifiedSourcer([]source.VerifiedCollection{ { @@ -262,7 +262,7 @@ func TestArtifacts(t *testing.T) { require.NoError(t, err) _, err = policy.Verify( context.Background(), - WithSubjectDigests([]string{dummySha}), + WithSubjectDigests([]cryptoutil.DigestSet{{cryptoutil.DigestValue{Hash: crypto.SHA256}: dummySha}}), WithVerifiedSource(newDummyVerifiedSourcer([]source.VerifiedCollection{ { Verifiers: []cryptoutil.Verifier{verifier}, @@ -301,7 +301,7 @@ func TestArtifacts(t *testing.T) { require.NoError(t, err) _, err = policy.Verify( context.Background(), - WithSubjectDigests([]string{dummySha}), + WithSubjectDigests([]cryptoutil.DigestSet{{cryptoutil.DigestValue{Hash: crypto.SHA256}: dummySha}}), WithVerifiedSource(newDummyVerifiedSourcer([]source.VerifiedCollection{ { Verifiers: []cryptoutil.Verifier{verifier}, @@ -381,6 +381,6 @@ func newDummyVerifiedSourcer(verifiedCollections []source.VerifiedCollection) *d 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 []cryptoutil.DigestSet, attestations []string) ([]source.VerifiedCollection, error) { return s.verifiedCollections, nil } diff --git a/source/archivista.go b/source/archivista.go index 66c4ec32..dd90b5b1 100644 --- a/source/archivista.go +++ b/source/archivista.go @@ -18,6 +18,7 @@ import ( "context" "github.com/testifysec/go-witness/archivista" + "github.com/testifysec/go-witness/cryptoutil" ) type ArchivistaSource struct { @@ -32,10 +33,16 @@ func NewArchvistSource(client *archivista.Client) *ArchivistaSource { } } -func (s *ArchivistaSource) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]CollectionEnvelope, error) { +func (s *ArchivistaSource) Search(ctx context.Context, collectionName string, subjectDigests []cryptoutil.DigestSet, attestations []string) ([]CollectionEnvelope, error) { + digestArray := make([]string, 0) + for _, ds := range subjectDigests { + for _, digest := range ds { + digestArray = append(digestArray, digest) + } + } gitoids, err := s.client.SearchGitoids(ctx, archivista.SearchGitoidVariables{ CollectionName: collectionName, - SubjectDigests: subjectDigests, + SubjectDigests: digestArray, Attestations: attestations, ExcludeGitoids: s.seenGitoids, }) diff --git a/source/memory.go b/source/memory.go index df6e5609..062d26f0 100644 --- a/source/memory.go +++ b/source/memory.go @@ -21,6 +21,7 @@ import ( "io" "os" + "github.com/testifysec/go-witness/cryptoutil" "github.com/testifysec/go-witness/dsse" ) @@ -103,7 +104,7 @@ func (s *MemorySource) LoadEnvelope(reference string, env dsse.Envelope) error { return nil } -func (s *MemorySource) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]CollectionEnvelope, error) { +func (s *MemorySource) Search(ctx context.Context, collectionName string, subjectDigests []cryptoutil.DigestSet, attestations []string) ([]CollectionEnvelope, error) { matches := make([]CollectionEnvelope, 0) for _, potentialMatchReference := range s.referencesByCollectionName[collectionName] { env, ok := s.envelopesByReference[potentialMatchReference] @@ -114,10 +115,12 @@ func (s *MemorySource) Search(ctx context.Context, collectionName string, subjec // make sure at least one of the subjects digests exists on the potential matches subjectMatchFound := false indexSubjects := s.subjectDigestsByReference[potentialMatchReference] - for _, checkDigest := range subjectDigests { - if _, ok := indexSubjects[checkDigest]; ok { - subjectMatchFound = true - break + for _, checkDigestSet := range subjectDigests { + for _, checkDigest := range checkDigestSet { + if _, ok := indexSubjects[checkDigest]; ok { + subjectMatchFound = true + break + } } } diff --git a/source/multi.go b/source/multi.go index d3611af4..3e36c3f9 100644 --- a/source/multi.go +++ b/source/multi.go @@ -14,7 +14,11 @@ package source -import "context" +import ( + "context" + + "github.com/testifysec/go-witness/cryptoutil" +) type MultiSource struct { sources []Sourcer @@ -24,7 +28,7 @@ func NewMultiSource(sources ...Sourcer) *MultiSource { return &MultiSource{sources} } -func (s *MultiSource) Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]CollectionEnvelope, error) { +func (s *MultiSource) Search(ctx context.Context, collectionName string, subjectDigests []cryptoutil.DigestSet, attestations []string) ([]CollectionEnvelope, error) { results := make([]CollectionEnvelope, 0) for _, source := range s.sources { res, err := source.Search(ctx, collectionName, subjectDigests, attestations) diff --git a/source/source.go b/source/source.go index 7ebd5e41..d15dcf26 100644 --- a/source/source.go +++ b/source/source.go @@ -19,6 +19,7 @@ import ( "encoding/json" "github.com/testifysec/go-witness/attestation" + "github.com/testifysec/go-witness/cryptoutil" "github.com/testifysec/go-witness/dsse" "github.com/testifysec/go-witness/intoto" ) @@ -31,7 +32,7 @@ type CollectionEnvelope struct { } type Sourcer interface { - Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]CollectionEnvelope, error) + Search(ctx context.Context, collectionName string, subjectDigests []cryptoutil.DigestSet, attestations []string) ([]CollectionEnvelope, error) } func envelopeToCollectionEnvelope(reference string, env dsse.Envelope) (CollectionEnvelope, error) { diff --git a/source/verified.go b/source/verified.go index 7b9c92bb..0fdf4df9 100644 --- a/source/verified.go +++ b/source/verified.go @@ -28,7 +28,7 @@ type VerifiedCollection struct { } type VerifiedSourcer interface { - Search(ctx context.Context, collectionName string, subjectDigests, attestations []string) ([]VerifiedCollection, error) + Search(ctx context.Context, collectionName string, subjectDigests []cryptoutil.DigestSet, attestations []string) ([]VerifiedCollection, error) } type VerifiedSource struct { @@ -40,7 +40,7 @@ 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 []cryptoutil.DigestSet, attestations []string) ([]VerifiedCollection, error) { unverified, err := s.source.Search(ctx, collectionName, subjectDigests, attestations) if err != nil { return nil, err