From 070f09d3e52ab6e795d6dac489730921d887efcb Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Mon, 16 Oct 2023 19:02:42 -0400 Subject: [PATCH] wip: implement verification attestor that returns a slsa VSA Co-authored-by: Kris Coleman --- attestation/verification/verification.go | 207 ++++++++++++++++++ attestation/verification/verification_test.go | 61 ++++++ dsse/sign.go | 5 - policy/policy.go | 18 +- run.go | 2 +- slsa/verificationsummary.go | 33 +++ verify.go | 115 ++++------ 7 files changed, 359 insertions(+), 82 deletions(-) create mode 100644 attestation/verification/verification.go create mode 100644 attestation/verification/verification_test.go create mode 100644 slsa/verificationsummary.go diff --git a/attestation/verification/verification.go b/attestation/verification/verification.go new file mode 100644 index 00000000..db986c6a --- /dev/null +++ b/attestation/verification/verification.go @@ -0,0 +1,207 @@ +package verification + +import ( + "crypto" + "crypto/x509" + "encoding/json" + "fmt" + "time" + + "github.com/testifysec/go-witness/attestation" + "github.com/testifysec/go-witness/cryptoutil" + "github.com/testifysec/go-witness/dsse" + "github.com/testifysec/go-witness/log" + "github.com/testifysec/go-witness/policy" + "github.com/testifysec/go-witness/slsa" + "github.com/testifysec/go-witness/source" + "github.com/testifysec/go-witness/timestamp" +) + +const ( + Name = "policyverify" + Type = slsa.VerificationSummaryPredicate +) + +var ( + _ attestation.Subjecter = &Attestor{} + _ attestation.Attestor = &Attestor{} +) + +type Attestor struct { + slsa.VerificationSummary + + policyEnvelope dsse.Envelope + policyVerifiers []cryptoutil.Verifier + collectionSource source.Sourcer + subjectDigests []string +} + +type Option func(*Attestor) + +func VerifyWithPolicyEnvelope(policyEnvelope dsse.Envelope) Option { + return func(vo *Attestor) { + vo.policyEnvelope = policyEnvelope + } +} + +func VerifyWithPolicyVerifiers(policyVerifiers []cryptoutil.Verifier) Option { + return func(vo *Attestor) { + vo.policyVerifiers = append(vo.policyVerifiers, policyVerifiers...) + } +} + +func VerifyWithSubjectDigests(subjectDigests []cryptoutil.DigestSet) Option { + return func(vo *Attestor) { + for _, set := range subjectDigests { + for _, digest := range set { + vo.subjectDigests = append(vo.subjectDigests, digest) + } + } + } +} + +func VerifyWithCollectionSource(source source.Sourcer) Option { + return func(vo *Attestor) { + vo.collectionSource = source + } +} + +func New(opts ...Option) *Attestor { + a := &Attestor{} + for _, opt := range opts { + opt(a) + } + + return a +} + +func (vs *Attestor) Name() string { + return Name +} + +func (vs *Attestor) Type() string { + return Type +} + +func (vs *Attestor) RunType() attestation.RunType { + return attestation.ExecuteRunType +} + +func (vs *Attestor) Subjects() map[string]cryptoutil.DigestSet { + panic("not implemented") // TODO: Implement +} + +func (vo *Attestor) Attest(ctx *attestation.AttestationContext) error { + if _, err := vo.policyEnvelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...)); err != nil { + return fmt.Errorf("could not verify policy: %w", err) + } + + pol := policy.Policy{} + if err := json.Unmarshal(vo.policyEnvelope.Payload, &pol); err != nil { + return fmt.Errorf("failed to unmarshal policy from envelope: %w", err) + } + + pubKeysById, err := pol.PublicKeyVerifiers() + if err != nil { + return fmt.Errorf("failed to get public keys from policy: %w", err) + } + + pubkeys := make([]cryptoutil.Verifier, 0) + for _, pubkey := range pubKeysById { + pubkeys = append(pubkeys, pubkey) + } + + trustBundlesById, err := pol.TrustBundles() + if err != nil { + return fmt.Errorf("failed to load policy trust bundles: %w", err) + } + + roots := make([]*x509.Certificate, 0) + intermediates := make([]*x509.Certificate, 0) + for _, trustBundle := range trustBundlesById { + roots = append(roots, trustBundle.Root) + intermediates = append(intermediates, trustBundle.Intermediates...) + } + + timestampAuthoritiesById, err := pol.TimestampAuthorityTrustBundles() + if err != nil { + return fmt.Errorf("failed to load policy timestamp authorities: %w", err) + } + + timestampVerifiers := make([]dsse.TimestampVerifier, 0) + for _, timestampAuthority := range timestampAuthoritiesById { + certs := []*x509.Certificate{timestampAuthority.Root} + certs = append(certs, timestampAuthority.Intermediates...) + timestampVerifiers = append(timestampVerifiers, timestamp.NewVerifier(timestamp.VerifyWithCerts(certs))) + } + + verifiedSource := source.NewVerifiedSource( + vo.collectionSource, + dsse.VerifyWithVerifiers(pubkeys...), + dsse.VerifyWithRoots(roots...), + dsse.VerifyWithIntermediates(intermediates...), + dsse.VerifyWithTimestampVerifiers(timestampVerifiers...), + ) + + policyResult, err := pol.Verify(ctx.Context(), policy.WithSubjectDigests(vo.subjectDigests), policy.WithVerifiedSource(verifiedSource)) + if _, ok := err.(policy.ErrPolicyDenied); ok { + vo.VerificationSummary, err = verificationSummaryFromResults(vo.policyEnvelope, policyResult, false) + if err != nil { + return fmt.Errorf("failed to generate verification summary: %w", err) + } + } else if err != nil { + return fmt.Errorf("failed to verify policy: %w", err) + } + + vo.VerificationSummary, err = verificationSummaryFromResults(vo.policyEnvelope, policyResult, true) + if err != nil { + return fmt.Errorf("failed to generate verification summary: %w", err) + } + return nil +} + +func calculateGitoid(b []byte) (cryptoutil.DigestSet, error) { + return cryptoutil.CalculateDigestSetFromBytes(b, []crypto.Hash{crypto.SHA256}) +} + +func verificationSummaryFromResults(policyEnvelope dsse.Envelope, policyResult policy.PolicyResult, accepted bool) (slsa.VerificationSummary, error) { + inputAttestations := make([]slsa.ResourceDescriptor, len(policyResult.EvidenceByStep)) + for _, input := range policyResult.EvidenceByStep { + for _, attestation := range input { + digest, err := calculateGitoid(attestation.Envelope.Payload) + if err != nil { + log.Debugf("failed to calculate evidence hash: %v", err) + continue + } + + inputAttestations = append(inputAttestations, slsa.ResourceDescriptor{ + URI: attestation.Reference, + Digest: digest, + }) + } + } + + policyDigest, err := calculateGitoid(policyEnvelope.Payload) + if err != nil { + return slsa.VerificationSummary{}, fmt.Errorf("failed to calculate policy digest: %w", err) + } + + verificationResult := slsa.FailedVerificationResult + if accepted { + verificationResult = slsa.PassedVerificationResult + } + + return slsa.VerificationSummary{ + Verifier: slsa.Verifier{ + ID: "witness", + }, + TimeVerified: time.Now(), + Policy: slsa.ResourceDescriptor{ + URI: "", + Digest: policyDigest, + }, + // ResourceURI: , + InputAttestations: inputAttestations, + VerificationResult: verificationResult, + }, nil +} diff --git a/attestation/verification/verification_test.go b/attestation/verification/verification_test.go new file mode 100644 index 00000000..4d2a48d6 --- /dev/null +++ b/attestation/verification/verification_test.go @@ -0,0 +1,61 @@ +// Copyright 2023 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 verification + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/testifysec/go-witness/attestation" +) + +func TestAttestorName(t *testing.T) { + a := New() + assert.Equal(t, a.Name(), "Expected Attestor Name Here") +} + +func TestAttestorType(t *testing.T) { + a := New() + assert.Equal(t, a.Type(), "Expected Attestor Type Here") +} + +func TestAttestorRunType(t *testing.T) { + a := New() + assert.Equal(t, a.RunType(), "Expected RunType Here") +} + +func TestAttestorAttest(t *testing.T) { + // Arrange + a := New() + ctx := &attestation.AttestationContext{} + + // Act + err := a.Attest(ctx) + + // Assert + require.NoError(t, err) +} + +func TestYourFunctionHere(t *testing.T) { + // Arrange + // Setup variables here + + // Act + // Perform function to be tested here + + // Assert + // Assert whether the expected result and actual result match or not. +} diff --git a/dsse/sign.go b/dsse/sign.go index 570934a3..2a1e4a85 100644 --- a/dsse/sign.go +++ b/dsse/sign.go @@ -18,7 +18,6 @@ import ( "bytes" "context" "encoding/pem" - "fmt" "io" "github.com/testifysec/go-witness/cryptoutil" @@ -54,10 +53,6 @@ func Sign(bodyType string, body io.Reader, opts ...SignOption) (Envelope, error) opt(so) } - if len(so.signers) == 0 { - return env, fmt.Errorf("must have at least one signer, have %v", len(so.signers)) - } - bodyBytes, err := io.ReadAll(body) if err != nil { return env, err diff --git a/policy/policy.go b/policy/policy.go index c4f57deb..a77b4e8c 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -168,7 +168,11 @@ func checkVerifyOpts(vo *verifyOptions) error { return nil } -func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][]source.VerifiedCollection, error) { +type PolicyResult struct { + EvidenceByStep map[string][]source.VerifiedCollection +} + +func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (PolicyResult, error) { vo := &verifyOptions{ searchDepth: 3, } @@ -178,16 +182,16 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][] } if err := checkVerifyOpts(vo); err != nil { - return nil, err + return PolicyResult{}, err } if time.Now().After(p.Expires.Time) { - return nil, ErrPolicyExpired(p.Expires.Time) + return PolicyResult{}, ErrPolicyExpired(p.Expires.Time) } trustBundles, err := p.TrustBundles() if err != nil { - return nil, err + return PolicyResult{}, err } attestationsByStep := make(map[string][]string) @@ -202,7 +206,7 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][] for stepName, step := range p.Steps { statements, err := vo.verifiedSource.Search(ctx, stepName, vo.subjectDigests, attestationsByStep[stepName]) if err != nil { - return nil, err + return PolicyResult{}, err } approvedCollections := step.checkFunctionaries(statements, trustBundles) @@ -218,11 +222,11 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (map[string][] } if accepted, err := p.verifyArtifacts(passedByStep); err == nil { - return accepted, nil + return PolicyResult{EvidenceByStep: accepted}, nil } } - return nil, ErrPolicyDenied{Reasons: []string{"failed to find set of attestations that satisfies the policy"}} + return PolicyResult{}, 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 diff --git a/run.go b/run.go index d9c489b3..f5718912 100644 --- a/run.go +++ b/run.go @@ -39,7 +39,7 @@ type RunOption func(ro *runOptions) func RunWithAttestors(attestors []attestation.Attestor) RunOption { return func(ro *runOptions) { - ro.attestors = attestors + ro.attestors = append(ro.attestors, attestors...) } } diff --git a/slsa/verificationsummary.go b/slsa/verificationsummary.go new file mode 100644 index 00000000..557c6e34 --- /dev/null +++ b/slsa/verificationsummary.go @@ -0,0 +1,33 @@ +package slsa + +import ( + "time" + + "github.com/testifysec/go-witness/cryptoutil" +) + +const ( + VerificationSummaryPredicate = "https://slsa.dev/verification_summary/v1" + PassedVerificationResult VerificationResult = "PASSED" + FailedVerificationResult VerificationResult = "FAILED" +) + +type VerificationResult string + +type Verifier struct { + ID string `json:"id"` +} + +type ResourceDescriptor struct { + URI string `json:"uri"` + Digest cryptoutil.DigestSet `json:"digest"` +} + +type VerificationSummary struct { + Verifier Verifier `json:"verifier"` + TimeVerified time.Time `json:"timeVerified"` + ResourceURI string `json:"resourceUri"` + Policy ResourceDescriptor `json:"policy"` + InputAttestations []ResourceDescriptor `json:"inputAttestations"` + VerificationResult VerificationResult `json:"verificationResult"` +} diff --git a/verify.go b/verify.go index daa91902..5a70dfea 100644 --- a/verify.go +++ b/verify.go @@ -16,16 +16,16 @@ package witness import ( "context" - "crypto/x509" "encoding/json" "fmt" "io" + "github.com/testifysec/go-witness/attestation" + "github.com/testifysec/go-witness/attestation/verification" "github.com/testifysec/go-witness/cryptoutil" "github.com/testifysec/go-witness/dsse" - "github.com/testifysec/go-witness/policy" + "github.com/testifysec/go-witness/slsa" "github.com/testifysec/go-witness/source" - "github.com/testifysec/go-witness/timestamp" ) func VerifySignature(r io.Reader, verifiers ...cryptoutil.Verifier) (dsse.Envelope, error) { @@ -40,96 +40,73 @@ func VerifySignature(r io.Reader, verifiers ...cryptoutil.Verifier) (dsse.Envelo } type verifyOptions struct { - policyEnvelope dsse.Envelope - policyVerifiers []cryptoutil.Verifier - collectionSource source.Sourcer - subjectDigests []string + attestorOptions []verification.Option + signer cryptoutil.Signer + stepName string + runOptions []RunOption } type VerifyOption func(*verifyOptions) -func VerifyWithSubjectDigests(subjectDigests []cryptoutil.DigestSet) VerifyOption { +func VerifyWithSigner(signer cryptoutil.Signer) VerifyOption { return func(vo *verifyOptions) { - for _, set := range subjectDigests { - for _, digest := range set { - vo.subjectDigests = append(vo.subjectDigests, digest) - } - } + vo.signer = signer } } -func VerifyWithCollectionSource(source source.Sourcer) VerifyOption { +func VerifyWithSubjectDigests(subjectDigests []cryptoutil.DigestSet) VerifyOption { return func(vo *verifyOptions) { - vo.collectionSource = source + vo.attestorOptions = append(vo.attestorOptions, verification.VerifyWithSubjectDigests(subjectDigests)) } } -// 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) { - vo := verifyOptions{ - policyEnvelope: policyEnvelope, - policyVerifiers: policyVerifiers, - } - - for _, opt := range opts { - opt(&vo) - } - - if _, err := vo.policyEnvelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...)); err != nil { - return nil, fmt.Errorf("could not verify policy: %w", err) - } - - 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) +func VerifyWithCollectionSource(source source.Sourcer) VerifyOption { + return func(vo *verifyOptions) { + vo.attestorOptions = append(vo.attestorOptions, verification.VerifyWithCollectionSource(source)) } +} - pubKeysById, err := pol.PublicKeyVerifiers() - if err != nil { - return nil, fmt.Errorf("failed to get pulic keys from policy: %w", err) +func VerifyWithAttestorOptions(opts ...verification.Option) VerifyOption { + return func(vo *verifyOptions) { + vo.attestorOptions = append(vo.attestorOptions, opts...) } +} - pubkeys := make([]cryptoutil.Verifier, 0) - for _, pubkey := range pubKeysById { - pubkeys = append(pubkeys, pubkey) +func VerifyWithRunOptions(opts ...RunOption) VerifyOption { + return func(vo *verifyOptions) { + vo.runOptions = append(vo.runOptions, opts...) } +} - trustBundlesById, err := pol.TrustBundles() - if err != nil { - return nil, fmt.Errorf("failed to load policy trust bundles: %w", err) - } +type VerifyResult struct { + RunResult + VerificationSummary slsa.VerificationSummary +} - roots := make([]*x509.Certificate, 0) - intermediates := make([]*x509.Certificate, 0) - for _, trustBundle := range trustBundlesById { - roots = append(roots, trustBundle.Root) - intermediates = append(intermediates, intermediates...) +// 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) (VerifyResult, error) { + vo := verifyOptions{ + stepName: "verify", } - timestampAuthoritiesById, err := pol.TimestampAuthorityTrustBundles() - if err != nil { - return nil, fmt.Errorf("failed to load policy timestamp authorities: %w", err) + for _, opt := range opts { + opt(&vo) } - timestampVerifiers := make([]dsse.TimestampVerifier, 0) - for _, timestampAuthority := range timestampAuthoritiesById { - certs := []*x509.Certificate{timestampAuthority.Root} - certs = append(certs, timestampAuthority.Intermediates...) - timestampVerifiers = append(timestampVerifiers, timestamp.NewVerifier(timestamp.VerifyWithCerts(certs))) - } + vo.attestorOptions = append(vo.attestorOptions, verification.VerifyWithPolicyEnvelope(policyEnvelope), verification.VerifyWithPolicyVerifiers(policyVerifiers)) - verifiedSource := source.NewVerifiedSource( - vo.collectionSource, - dsse.VerifyWithVerifiers(pubkeys...), - dsse.VerifyWithRoots(roots...), - dsse.VerifyWithIntermediates(intermediates...), - dsse.VerifyWithTimestampVerifiers(timestampVerifiers...), + // hacky solution to ensure the verification attestor is run through the attestation context + vo.runOptions = append(vo.runOptions, + RunWithAttestors( + []attestation.Attestor{ + verification.New(vo.attestorOptions...), + }, + ), ) - accepted, 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 accepted, nil + runResult, err := Run(vo.stepName, vo.signer, vo.runOptions...) + return VerifyResult{ + RunResult: runResult, + }, err }