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 <[email protected]>
  • Loading branch information
mikhailswift and kriscoleman committed Oct 27, 2023
1 parent 69cb3ee commit e618e52
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 79 deletions.
213 changes: 213 additions & 0 deletions attestation/policyverify/policyverify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package policyverify

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 {
subjects := map[string]cryptoutil.DigestSet{}
for _, digest := range vs.subjectDigests {
subjects[fmt.Sprintf("artifact:%v", digest)] = cryptoutil.DigestSet{
cryptoutil.DigestValue{Hash: crypto.SHA256, GitOID: false}: digest,
}
}

subjects[fmt.Sprintf("policy:%v", vs.VerificationSummary.Policy.URI)] = vs.VerificationSummary.Policy.Digest
return subjects
}

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...),
)

accepted := true
policyResult, policyErr := pol.Verify(ctx.Context(), policy.WithSubjectDigests(vo.subjectDigests), policy.WithVerifiedSource(verifiedSource))
if _, ok := policyErr.(policy.ErrPolicyDenied); ok {
accepted = false
} else if policyErr != nil {
return fmt.Errorf("failed to verify policy: %w", err)
}

vo.VerificationSummary, err = verificationSummaryFromResults(vo.policyEnvelope, policyResult, accepted)
if err != nil {
return fmt.Errorf("failed to generate verification summary: %w", err)
}

return policyErr
}

func calculateDigest(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, 0, len(policyResult.EvidenceByStep))
for _, input := range policyResult.EvidenceByStep {
for _, attestation := range input {
digest, err := calculateDigest(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 := calculateDigest(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: policyDigest[cryptoutil.DigestValue{Hash: crypto.SHA256, GitOID: false}], //TODO: find a better value for this...
Digest: policyDigest,
},
InputAttestations: inputAttestations,
VerificationResult: verificationResult,
}, nil
}
61 changes: 61 additions & 0 deletions attestation/policyverify/policyverify_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 policyverify

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.
}
4 changes: 4 additions & 0 deletions dsse/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ func Sign(bodyType string, body io.Reader, opts ...SignOption) (Envelope, error)
env.Signatures = make([]Signature, 0)
pae := preauthEncode(bodyType, bodyBytes)
for _, signer := range so.signers {
if signer == nil {
continue
}

sig, err := signer.Sign(bytes.NewReader(pae))
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
Loading

0 comments on commit e618e52

Please sign in to comment.