diff --git a/dsse/verify.go b/dsse/verify.go index d77e83c5..9c94a744 100644 --- a/dsse/verify.go +++ b/dsse/verify.go @@ -72,8 +72,6 @@ type CheckedVerifier struct { Error error } -type FailedVerifier struct{} - func (e Envelope) Verify(opts ...VerificationOption) ([]CheckedVerifier, error) { options := &verificationOptions{ threshold: 1, diff --git a/policy/policy.go b/policy/policy.go index 5ab2408e..946f3ed8 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -250,9 +250,9 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (bool, map[str } // Verify the functionaries - collections = step.checkFunctionaries(collections, trustBundles) - - stepResult := step.validateAttestations(collections) + functionaryCheckResults := step.checkFunctionaries(collections, trustBundles) + stepResult := step.validateAttestations(functionaryCheckResults.Passed) + stepResult.Rejected = append(stepResult.Rejected, functionaryCheckResults.Rejected...) // We perform many searches against the same step, so we need to merge the relevant fields if resultsByStep[stepName].Step == "" { @@ -293,11 +293,12 @@ func (p Policy) Verify(ctx context.Context, opts ...VerifyOption) (bool, map[str // 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(statements []source.CollectionVerificationResult, trustBundles map[string]TrustBundle) []source.CollectionVerificationResult { +func (step Step) checkFunctionaries(statements []source.CollectionVerificationResult, trustBundles map[string]TrustBundle) StepResult { + result := StepResult{Step: step.Name} for i, statement := range statements { // Check that the statement contains a predicate type that we accept if statement.Statement.PredicateType != attestation.CollectionType { - statements[i].Errors = append(statement.Errors, fmt.Errorf("predicate type %v is not a collection predicate type", statement.Statement.PredicateType)) + result.Rejected = append(result.Rejected, RejectedCollection{Collection: statement, Reason: fmt.Errorf("predicate type %v is not a collection predicate type", statement.Statement.PredicateType)}) } if len(statement.Verifiers) > 0 { @@ -311,12 +312,18 @@ func (step Step) checkFunctionaries(statements []source.CollectionVerificationRe } } } + + if len(statements[i].ValidFunctionaries) == 0 { + result.Rejected = append(result.Rejected, RejectedCollection{Collection: statements[i], Reason: fmt.Errorf("no verifiers matched with allowed functionaries for step %s", step.Name)}) + } else { + result.Passed = append(result.Passed, statements[i]) + } } else { - statements[i].Errors = append(statement.Errors, fmt.Errorf("no verifiers present to validate against collection verifiers")) + result.Rejected = append(result.Rejected, RejectedCollection{Collection: statements[i], Reason: fmt.Errorf("no verifiers present to validate against collection verifiers")}) } } - return statements + return result } // verifyArtifacts will check the artifacts (materials+products) of the step referred to by `ArtifactsFrom` against the diff --git a/policy/policy_test.go b/policy/policy_test.go index 761237c5..e3a766fd 100644 --- a/policy/policy_test.go +++ b/policy/policy_test.go @@ -588,7 +588,7 @@ func TestCheckFunctionaries(t *testing.T) { fmt.Println("running test case: ", testCase.name) result := testCase.step.checkFunctionaries(testCase.statements, testCase.trustBundles) resultCheckFields := []source.CollectionVerificationResult{} - for _, r := range result { + for _, r := range result.Passed { o := source.CollectionVerificationResult{ Errors: r.Errors, Warnings: r.Warnings, @@ -597,6 +597,15 @@ func TestCheckFunctionaries(t *testing.T) { resultCheckFields = append(resultCheckFields, o) } + for _, r := range result.Rejected { + o := source.CollectionVerificationResult{ + Errors: r.Collection.Errors, + Warnings: r.Collection.Warnings, + ValidFunctionaries: r.Collection.ValidFunctionaries, + } + resultCheckFields = append(resultCheckFields, o) + } + assert.Equal(t, testCase.expectedResults, resultCheckFields) } } diff --git a/source/verified.go b/source/verified.go index fab6404b..4f287c44 100644 --- a/source/verified.go +++ b/source/verified.go @@ -64,7 +64,9 @@ func (s *VerifiedSource) Search(ctx context.Context, collectionName string, subj passedVerifiers := make([]cryptoutil.Verifier, 0) for _, verifier := range envelopeVerifiers { - passedVerifiers = append(passedVerifiers, verifier.Verifier) + if verifier.Error == nil { + passedVerifiers = append(passedVerifiers, verifier.Verifier) + } } var Errors []error diff --git a/verify_test.go b/verify_test.go index 21534b24..991905e4 100644 --- a/verify_test.go +++ b/verify_test.go @@ -34,15 +34,14 @@ import ( "github.com/in-toto/go-witness/dsse" "github.com/in-toto/go-witness/policy" "github.com/in-toto/go-witness/source" + "github.com/invopop/jsonschema" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestVerify(t *testing.T) { - testPolicy, functionarySigner := makepolicyRSA(t) - policyEnvelope, policySigner := signPolicyRSA(t, testPolicy) - policyVerifier, err := policySigner.Verifier() - require.NoError(t, err) + testPolicy, functionarySigner := makePolicyWithPublicKeyFunctionary(t) + policyEnvelope, _, policyVerifier := signPolicyRSA(t, testPolicy) workingDir := t.TempDir() step1Result, err := Run( @@ -136,27 +135,46 @@ func TestVerify(t *testing.T) { t.Run("Fail with missing attestation", func(t *testing.T) { functionaryVerifier, err := functionarySigner.Verifier() require.NoError(t, err) - functionaryKeyID, err := functionaryVerifier.KeyID() - require.NoError(t, err) - functionaryPublicKey, err := functionaryVerifier.Bytes() - require.NoError(t, err) - failPolicy := makepolicy(policy.Functionary{ - Type: "PublicKey", - PublicKeyID: functionaryKeyID, - }, - policy.PublicKey{ - KeyID: functionaryKeyID, - Key: functionaryPublicKey, - }, - map[string]policy.Root{}, - ) + policyFunctionary, policyPk := functionaryFromVerifier(t, functionaryVerifier) + failPolicy := makePolicy(policyFunctionary, policyPk, map[string]policy.Root{}) step1 := failPolicy.Steps["step01"] step1.Attestations = append(step1.Attestations, policy.Attestation{Type: "nonexistent atttestation"}) failPolicy.Steps["step01"] = step1 - failPolicyEnvelope, failPolicySigner := signPolicyRSA(t, failPolicy) - failPolicyVerifier, err := failPolicySigner.Verifier() + failPolicyEnvelope, _, failPolicyVerifier := signPolicyRSA(t, failPolicy) + + memorySource := source.NewMemorySource() + require.NoError(t, memorySource.LoadEnvelope("step01", step1Result.SignedEnvelope)) + require.NoError(t, memorySource.LoadEnvelope("step02", step2Result.SignedEnvelope)) + + results, err := Verify( + context.Background(), + failPolicyEnvelope, + []cryptoutil.Verifier{failPolicyVerifier}, + VerifyWithCollectionSource(memorySource), + VerifyWithSubjectDigests(subjects), + ) + + require.Error(t, err, fmt.Sprintf("passed with results: %+v", results)) + }) + + t.Run("Fail with incorrect signer", func(t *testing.T) { + functionaryVerifier, err := functionarySigner.Verifier() require.NoError(t, err) + policyFunctionary, policyPk := functionaryFromVerifier(t, functionaryVerifier) + failPolicy := makePolicy(policyFunctionary, policyPk, map[string]policy.Root{}) + + // create a new key and functionary, and replace the step's functionary with it. + // the attestation would not have been signed with this key, so verification should fail. + newSigner := createTestRSAKey(t) + newVerifier, err := newSigner.Verifier() + require.NoError(t, err) + failPolicyFunctionary, failPolicyPk := functionaryFromVerifier(t, newVerifier) + failPolicy.PublicKeys[failPolicyPk.KeyID] = failPolicyPk + step1 := failPolicy.Steps["step01"] + step1.Functionaries = []policy.Functionary{failPolicyFunctionary} + failPolicy.Steps["step01"] = step1 + failPolicyEnvelope, _, failPolicyVerifier := signPolicyRSA(t, failPolicy) memorySource := source.NewMemorySource() require.NoError(t, memorySource.LoadEnvelope("step01", step1Result.SignedEnvelope)) @@ -174,7 +192,72 @@ func TestVerify(t *testing.T) { }) } -func makepolicy(functionary policy.Functionary, publicKey policy.PublicKey, roots map[string]policy.Root) policy.Policy { +func TestBackRefs(t *testing.T) { + registerDummyAttestors() + testPolicy, functionarySigner := makePolicyWithPublicKeyFunctionary(t) + policyEnvelope, _, policyVerifier := signPolicyRSA(t, testPolicy) + workingDir := t.TempDir() + + step1Result, err := Run( + "step01", + RunWithSigners(functionarySigner), + RunWithAttestors([]attestation.Attestor{ + material.New(), + &dummySubjectAttestor{Data: "test"}, + commandrun.New( + commandrun.WithCommand([]string{"bash", "-c", "echo 'test01' > test.txt"}), + ), + product.New(), + }), + RunWithAttestationOpts( + attestation.WithWorkingDir(workingDir), + ), + ) + require.NoError(t, err) + + step2Result, err := Run( + "step02", + RunWithSigners(functionarySigner), + RunWithAttestors([]attestation.Attestor{ + material.New(), + &dummyBackrefAttestor{}, + commandrun.New( + commandrun.WithCommand([]string{"bash", "-c", "echo 'test02' >> test.txt"}), + ), + product.New(), + }), + RunWithAttestationOpts( + attestation.WithWorkingDir(workingDir), + ), + ) + require.NoError(t, err) + + artifactSubject, err := cryptoutil.CalculateDigestSetFromFile( + filepath.Join(workingDir, "test.txt"), + []cryptoutil.DigestValue{ + { + GitOID: false, + Hash: crypto.SHA256, + }, + }, + ) + require.NoError(t, err) + memorySource := source.NewMemorySource() + require.NoError(t, memorySource.LoadEnvelope("step01", step1Result.SignedEnvelope)) + require.NoError(t, memorySource.LoadEnvelope("step02", step2Result.SignedEnvelope)) + + results, err := Verify( + context.Background(), + policyEnvelope, + []cryptoutil.Verifier{policyVerifier}, + VerifyWithCollectionSource(memorySource), + VerifyWithSubjectDigests([]cryptoutil.DigestSet{artifactSubject}), + ) + + require.NoError(t, err, fmt.Sprintf("failed with results: %+v", results)) +} + +func makePolicy(functionary policy.Functionary, publicKey policy.PublicKey, roots map[string]policy.Root) policy.Policy { step01 := policy.Step{ Name: "step01", Functionaries: []policy.Functionary{functionary}, @@ -209,33 +292,39 @@ func makepolicy(functionary policy.Functionary, publicKey policy.PublicKey, root return p } -func makepolicyRSA(t *testing.T) (policy.Policy, cryptoutil.Signer) { - signer, err := createTestRSAKey() - require.NoError(t, err) +func makePolicyWithPublicKeyFunctionary(t *testing.T) (policy.Policy, cryptoutil.Signer) { + signer := createTestRSAKey(t) verifier, err := signer.Verifier() require.NoError(t, err) - keyID, err := verifier.KeyID() - require.NoError(t, err) - functionary := policy.Functionary{ - Type: "PublicKey", - PublicKeyID: keyID, - } + functionary, pk := functionaryFromVerifier(t, verifier) + p := makePolicy(functionary, pk, nil) + return p, signer +} - pub, err := verifier.Bytes() +func functionaryFromVerifier(t *testing.T, v cryptoutil.Verifier) (policy.Functionary, policy.PublicKey) { + keyID, err := v.KeyID() require.NoError(t, err) - - pk := policy.PublicKey{ - KeyID: keyID, - Key: pub, - } - - p := makepolicy(functionary, pk, nil) - return p, signer + keyBytes, err := v.Bytes() + require.NoError(t, err) + return policy.Functionary{ + Type: "PublicKey", + PublicKeyID: keyID, + }, + policy.PublicKey{ + KeyID: keyID, + Key: keyBytes, + } } -func signPolicyRSA(t *testing.T, p policy.Policy) (dsse.Envelope, cryptoutil.Signer) { - signer, err := createTestRSAKey() +func signPolicyRSA(t *testing.T, p policy.Policy) (dsse.Envelope, cryptoutil.Signer, cryptoutil.Verifier) { + signer := createTestRSAKey(t) + env := signPolicy(t, p, signer) + verifier, err := signer.Verifier() require.NoError(t, err) + return env, signer, verifier +} + +func signPolicy(t *testing.T, p policy.Policy, signer cryptoutil.Signer) dsse.Envelope { pBytes, err := json.Marshal(p) require.NoError(t, err) reader := bytes.NewReader(pBytes) @@ -244,15 +333,98 @@ func signPolicyRSA(t *testing.T, p policy.Policy) (dsse.Envelope, cryptoutil.Sig require.NoError(t, Sign(reader, policy.PolicyPredicate, writer, dsse.SignWithSigners(signer))) env := dsse.Envelope{} require.NoError(t, json.Unmarshal(writer.Bytes(), &env)) - return env, signer + return env } -func createTestRSAKey() (cryptoutil.Signer, error) { +func createTestRSAKey(t *testing.T) cryptoutil.Signer { privKey, err := rsa.GenerateKey(rand.Reader, 512) - if err != nil { - return nil, err + require.NoError(t, err) + signer := cryptoutil.NewRSASigner(privKey, crypto.SHA256) + return signer +} + +const ( + dummySubjectAttestorName = "subject attestor" + dummySubjectAttestorType = "test/subjectattestor" + dummyBackrefAttestorName = "backref attestor" + dummyBackrefAttestorType = "test/backrefattestor" + matchSubjectName = "matchSubject" +) + +// policy verification currently needs attestors to be registers to properly validate them +func registerDummyAttestors() { + attestation.RegisterAttestation(dummyBackrefAttestorName, dummyBackrefAttestorType, attestation.PreMaterialRunType, func() attestation.Attestor { return &dummyBackrefAttestor{} }) + attestation.RegisterAttestation(dummySubjectAttestorName, dummySubjectAttestorType, attestation.PreMaterialRunType, func() attestation.Attestor { return &dummySubjectAttestor{} }) +} + +// dummySubjectAttestor is a test attestor used to create a subject on an attestation. +// this subject will be used to discover this attestor when searching by back ref subjects +// from a subsequent step in the policy. +type dummySubjectAttestor struct { + Data string +} + +func (a *dummySubjectAttestor) Name() string { + return dummySubjectAttestorName +} + +func (a *dummySubjectAttestor) Type() string { + return dummySubjectAttestorType +} + +func (a *dummySubjectAttestor) RunType() attestation.RunType { + return attestation.PreMaterialRunType +} + +func (a *dummySubjectAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (a *dummySubjectAttestor) Schema() *jsonschema.Schema { + return nil +} + +func (a *dummySubjectAttestor) Subjects() map[string]cryptoutil.DigestSet { + return map[string]cryptoutil.DigestSet{ + matchSubjectName: { + {Hash: crypto.SHA256}: "abcde", + }, } +} - signer := cryptoutil.NewRSASigner(privKey, crypto.SHA256) - return signer, nil +// dummyBackrefAttestor is a test attestor used to expose a back ref subject, used to find +// attestations from preceding steps. +// for a practical example of this, consider policy that enforces two steps: a test step and a build step that produces a binary. +// when we begin policy evaluation, we only know two things: the hash of the binary, and the steps the policy expects. +// when we look up attestations that contain a product matching the binary's hash and satisfies the build step of the policy. +// that build attestation may contain a back ref subject that is the hash of the git commit, which also appears on the test attestation. +// we can then use this back ref subject to link the test attestation to the build attestation during policy evaluation. +type dummyBackrefAttestor struct{} + +func (a *dummyBackrefAttestor) Name() string { + return dummyBackrefAttestorName +} + +func (a *dummyBackrefAttestor) Type() string { + return dummyBackrefAttestorType +} + +func (a *dummyBackrefAttestor) RunType() attestation.RunType { + return attestation.PreMaterialRunType +} + +func (a *dummyBackrefAttestor) Attest(ctx *attestation.AttestationContext) error { + return nil +} + +func (a *dummyBackrefAttestor) Schema() *jsonschema.Schema { + return nil +} + +func (a *dummyBackrefAttestor) BackRefs() map[string]cryptoutil.DigestSet { + return map[string]cryptoutil.DigestSet{ + matchSubjectName: { + {Hash: crypto.SHA256}: "abcde", + }, + } }