Skip to content

Commit

Permalink
wip: implement verification attestor that returns a slsa VSA
Browse files Browse the repository at this point in the history
Co-authored-by: Kris Coleman <kriscodeman@gmail.com>
  • Loading branch information
mikhailswift and kriscoleman committed Oct 23, 2023
1 parent 03cf3f0 commit 070f09d
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 82 deletions.
207 changes: 207 additions & 0 deletions attestation/verification/verification.go
Original file line number Diff line number Diff line change
@@ -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
}
61 changes: 61 additions & 0 deletions attestation/verification/verification_test.go
Original file line number Diff line number Diff line change
@@ -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.
}
5 changes: 0 additions & 5 deletions dsse/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"bytes"
"context"
"encoding/pem"
"fmt"
"io"

"github.com/testifysec/go-witness/cryptoutil"
Expand Down Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
}
}

Expand Down
33 changes: 33 additions & 0 deletions slsa/verificationsummary.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading

0 comments on commit 070f09d

Please sign in to comment.