diff --git a/attestation/context.go b/attestation/context.go index bef8e7bc..7980b223 100644 --- a/attestation/context.go +++ b/attestation/context.go @@ -33,12 +33,17 @@ const ( ExecuteRunType RunType = "execute" ProductRunType RunType = "product" PostProductRunType RunType = "postproduct" + VerifyRunType RunType = "verify" ) func runTypeOrder() []RunType { return []RunType{PreMaterialRunType, MaterialRunType, ExecuteRunType, ProductRunType, PostProductRunType} } +func verifyTypeOrder() []RunType { + return []RunType{VerifyRunType} +} + func (r RunType) String() string { return string(r) } @@ -131,11 +136,16 @@ func (ctx *AttestationContext) RunAttestors() error { Reason: "attestor run type not set", } } - attestors[attestor.RunType()] = append(attestors[attestor.RunType()], attestor) } order := runTypeOrder() + if attestors[VerifyRunType] != nil && len(attestors) > 1 { + return fmt.Errorf("attestors of type %s cannot be run in conjunction with other attestor types", VerifyRunType) + } else if attestors[VerifyRunType] != nil { + order = verifyTypeOrder() + } + for _, k := range order { log.Debugf("Starting %s attestors...", k.String()) for _, att := range attestors[k] { diff --git a/attestation/policyverify/policyverify.go b/attestation/policyverify/policyverify.go new file mode 100644 index 00000000..7266a660 --- /dev/null +++ b/attestation/policyverify/policyverify.go @@ -0,0 +1,244 @@ +// 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 ( + "crypto" + "crypto/x509" + "encoding/json" + "fmt" + "time" + + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/dsse" + ipolicy "github.com/in-toto/go-witness/internal/policy" + "github.com/in-toto/go-witness/log" + "github.com/in-toto/go-witness/policy" + "github.com/in-toto/go-witness/slsa" + "github.com/in-toto/go-witness/source" + "github.com/in-toto/go-witness/timestamp" +) + +const ( + Name = "policyverify" + Type = slsa.VerificationSummaryPredicate + RunType = attestation.VerifyRunType +) + +var ( + _ attestation.Subjecter = &Attestor{} + _ attestation.Attestor = &Attestor{} +) + +func init() { + attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { + return New() + }) +} + +type Attestor struct { + *ipolicy.VerifyPolicySignatureOptions + slsa.VerificationSummary + + stepResults map[string]policy.StepResult + policyEnvelope dsse.Envelope + collectionSource source.Sourcer + subjectDigests []string +} + +type Option func(*Attestor) + +func VerifyWithPolicyVerificationOptions(opts ...ipolicy.Option) Option { + return func(a *Attestor) { + for _, opt := range opts { + opt(a.VerifyPolicySignatureOptions) + } + } +} + +func VerifyWithPolicyEnvelope(policyEnvelope dsse.Envelope) Option { + return func(a *Attestor) { + a.policyEnvelope = policyEnvelope + } +} + +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 { + vps := ipolicy.NewVerifyPolicySignatureOptions() + a := &Attestor{ + VerifyPolicySignatureOptions: vps, + } + + for _, opt := range opts { + opt(a) + } + + return a +} + +func (a *Attestor) Name() string { + return Name +} + +func (a *Attestor) Type() string { + return Type +} + +func (a *Attestor) RunType() attestation.RunType { + return 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, + } + } + + subjects[fmt.Sprintf("policy:%v", a.VerificationSummary.Policy.URI)] = a.VerificationSummary.Policy.Digest + return subjects +} + +func (a *Attestor) StepResults() map[string]policy.StepResult { + return a.stepResults +} + +func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { + if err := ipolicy.VerifyPolicySignature(ctx.Context(), a.policyEnvelope, a.VerifyPolicySignatureOptions); err != nil { + return fmt.Errorf("failed to verify policy signature: %w", err) + } + + log.Info("policy signature verified") + + pol := policy.Policy{} + if err := json.Unmarshal(a.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, intermediates...) + } + + timestampAuthoritiesById, err := pol.TimestampAuthorityTrustBundles() + if err != nil { + return fmt.Errorf("failed to load policy timestamp authorities: %w", err) + } + + timestampVerifiers := make([]timestamp.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( + a.collectionSource, + dsse.VerifyWithVerifiers(pubkeys...), + dsse.VerifyWithRoots(roots...), + dsse.VerifyWithIntermediates(intermediates...), + dsse.VerifyWithTimestampVerifiers(timestampVerifiers...), + ) + + accepted, stepResults, policyErr := pol.Verify(ctx.Context(), policy.WithSubjectDigests(a.subjectDigests), policy.WithVerifiedSource(verifiedSource)) + if policyErr != nil { + // TODO: log stepResults + return fmt.Errorf("failed to verify policy: %w", policyErr) + } + + a.stepResults = stepResults + + a.VerificationSummary, err = verificationSummaryFromResults(ctx, a.policyEnvelope, stepResults, accepted) + if err != nil { + return fmt.Errorf("failed to generate verification summary: %w", err) + } + + return nil +} + +func verificationSummaryFromResults(ctx *attestation.AttestationContext, policyEnvelope dsse.Envelope, stepResults map[string]policy.StepResult, accepted bool) (slsa.VerificationSummary, error) { + inputAttestations := make([]slsa.ResourceDescriptor, 0, len(stepResults)) + for _, step := range stepResults { + for _, collection := range step.Passed { + digest, err := cryptoutil.CalculateDigestSetFromBytes(collection.Envelope.Payload, ctx.Hashes()) + if err != nil { + log.Debugf("failed to calculate evidence hash: %v", err) + continue + } + + inputAttestations = append(inputAttestations, slsa.ResourceDescriptor{ + URI: collection.Reference, + Digest: digest, + }) + } + } + + policyDigest, err := cryptoutil.CalculateDigestSetFromBytes(policyEnvelope.Payload, ctx.Hashes()) + 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: policy.PolicyPredicate, + Digest: policyDigest, + }, + InputAttestations: inputAttestations, + VerificationResult: verificationResult, + }, nil +} diff --git a/attestation/policyverify/policyverify_test.go b/attestation/policyverify/policyverify_test.go new file mode 100644 index 00000000..c3794c67 --- /dev/null +++ b/attestation/policyverify/policyverify_test.go @@ -0,0 +1,15 @@ +// 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 diff --git a/dsse/sign.go b/dsse/sign.go index 7092203c..267ec079 100644 --- a/dsse/sign.go +++ b/dsse/sign.go @@ -65,6 +65,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 diff --git a/imports.go b/imports.go index c368d889..f6580d39 100644 --- a/imports.go +++ b/imports.go @@ -28,6 +28,7 @@ import ( _ "github.com/in-toto/go-witness/attestation/material" _ "github.com/in-toto/go-witness/attestation/maven" _ "github.com/in-toto/go-witness/attestation/oci" + _ "github.com/in-toto/go-witness/attestation/policyverify" _ "github.com/in-toto/go-witness/attestation/product" _ "github.com/in-toto/go-witness/attestation/sarif" diff --git a/internal/policy/policy.go b/internal/policy/policy.go new file mode 100644 index 00000000..d1d32aee --- /dev/null +++ b/internal/policy/policy.go @@ -0,0 +1,151 @@ +// 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 policy + +import ( + "context" + "crypto/x509" + "encoding/base64" + "fmt" + + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/dsse" + "github.com/in-toto/go-witness/log" + "github.com/in-toto/go-witness/policy" + "github.com/in-toto/go-witness/timestamp" +) + +type VerifyPolicySignatureOptions struct { + policyVerifiers []cryptoutil.Verifier + policyTimestampAuthorities []timestamp.TimestampVerifier + policyCARoots []*x509.Certificate + policyCAIntermediates []*x509.Certificate + policyCommonName string + policyDNSNames []string + policyEmails []string + policyOrganizations []string + policyURIs []string +} + +type Option func(*VerifyPolicySignatureOptions) + +func VerifyWithPolicyVerifiers(policyVerifiers []cryptoutil.Verifier) Option { + return func(vo *VerifyPolicySignatureOptions) { + vo.policyVerifiers = append(vo.policyVerifiers, policyVerifiers...) + } +} + +func VerifyWithPolicyTimestampAuthorities(authorities []timestamp.TimestampVerifier) Option { + return func(vo *VerifyPolicySignatureOptions) { + vo.policyTimestampAuthorities = authorities + } +} + +func VerifyWithPolicyCARoots(roots []*x509.Certificate) Option { + return func(vo *VerifyPolicySignatureOptions) { + vo.policyCARoots = roots + } +} + +func VerifyWithPolicyCAIntermediates(intermediates []*x509.Certificate) Option { + return func(vo *VerifyPolicySignatureOptions) { + vo.policyCAIntermediates = intermediates + } +} + +func NewVerifyPolicySignatureOptions(opts ...Option) *VerifyPolicySignatureOptions { + vo := &VerifyPolicySignatureOptions{ + policyCommonName: "*", + policyDNSNames: []string{"*"}, + policyOrganizations: []string{"*"}, + policyURIs: []string{"*"}, + policyEmails: []string{"*"}, + } + + for _, opt := range opts { + opt(vo) + } + + return vo +} + +func VerifyWithPolicyCertConstraints(commonName string, dnsNames []string, emails []string, organizations []string, uris []string) Option { + return func(vo *VerifyPolicySignatureOptions) { + vo.policyCommonName = commonName + vo.policyDNSNames = dnsNames + vo.policyEmails = emails + vo.policyOrganizations = organizations + vo.policyURIs = uris + } +} + +func VerifyPolicySignature(ctx context.Context, envelope dsse.Envelope, vo *VerifyPolicySignatureOptions) error { + passedPolicyVerifiers, err := envelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...), dsse.VerifyWithTimestampVerifiers(vo.policyTimestampAuthorities...), dsse.VerifyWithRoots(vo.policyCARoots...), dsse.VerifyWithIntermediates(vo.policyCAIntermediates...)) + if err != nil { + return fmt.Errorf("could not verify policy: %w", err) + } + + var passed bool + for _, verifier := range passedPolicyVerifiers { + kid, err := verifier.Verifier.KeyID() + if err != nil { + return fmt.Errorf("could not get verifier key id: %w", err) + } + + var f policy.Functionary + trustBundle := make(map[string]policy.TrustBundle) + if _, ok := verifier.Verifier.(*cryptoutil.X509Verifier); ok { + rootIDs := make([]string, 0) + for _, root := range vo.policyCARoots { + id := base64.StdEncoding.EncodeToString(root.Raw) + rootIDs = append(rootIDs, id) + trustBundle[id] = policy.TrustBundle{ + Root: root, + } + } + + f = policy.Functionary{ + Type: "root", + CertConstraint: policy.CertConstraint{ + Roots: rootIDs, + CommonName: vo.policyCommonName, + URIs: vo.policyURIs, + Emails: vo.policyEmails, + Organizations: vo.policyOrganizations, + DNSNames: vo.policyDNSNames, + }, + } + + } else { + f = policy.Functionary{ + Type: "key", + PublicKeyID: kid, + } + } + + err = f.Validate(verifier.Verifier, trustBundle) + if err != nil { + log.Debugf("Policy Verifier %s failed failed to match supplied constraints: %w, continuing...", kid, err) + continue + } + passed = true + } + + if !passed { + return fmt.Errorf("no policy verifiers passed verification") + } else { + return nil + } +} diff --git a/verify_test.go b/internal/policy/policy_test.go similarity index 77% rename from verify_test.go rename to internal/policy/policy_test.go index adfd101c..32495af5 100644 --- a/verify_test.go +++ b/internal/policy/policy_test.go @@ -1,10 +1,10 @@ -// Copyright 2024 The Witness Contributors +// 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 +// 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, @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package witness +package policy import ( "bytes" "context" "crypto/x509" - "fmt" "testing" "time" @@ -26,6 +25,8 @@ import ( "github.com/in-toto/go-witness/internal/test" "github.com/in-toto/go-witness/intoto" "github.com/in-toto/go-witness/timestamp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/in-toto/go-witness/cryptoutil" ) @@ -33,29 +34,15 @@ import ( func TestVerifyPolicySignature(t *testing.T) { // we dont care about the content of th envelope for this test rsaSigner, rsaVerifier, _, err := test.CreateTestKey() - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) badRootCert, _, err := test.CreateRoot() - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) rootCert, key, err := test.CreateRoot() - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) leafCert, leafPriv, err := test.CreateLeaf(rootCert, key) - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) x509Signer, err := cryptoutil.NewSigner(leafPriv, cryptoutil.SignWithCertificate(leafCert)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) timestampers := []timestamp.FakeTimestamper{ {T: time.Now()}, @@ -70,7 +57,7 @@ func TestVerifyPolicySignature(t *testing.T) { timestampers []timestamp.FakeTimestamper Roots []*x509.Certificate Intermediates []*x509.Certificate - certConstraints VerifyOption + certConstraints Option wantErr bool }{ { @@ -127,37 +114,22 @@ func TestVerifyPolicySignature(t *testing.T) { } env, err := dsse.Sign(intoto.PayloadType, bytes.NewReader([]byte("this is some test data")), dsse.SignWithTimestampers(ts...), dsse.SignWithSigners(tt.signer)) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var tv []timestamp.TimestampVerifier for _, t := range tt.timestampers { tv = append(tv, t) } - vo := verifyOptions{ - policyEnvelope: env, - policyVerifiers: []cryptoutil.Verifier{tt.verifier}, - policyCARoots: tt.Roots, - policyTimestampAuthorities: tv, - policyCommonName: "*", - policyDNSNames: []string{"*"}, - policyOrganizations: []string{"*"}, - policyURIs: []string{"*"}, - policyEmails: []string{"*"}, - } - + o := []Option{} + o = append(o, VerifyWithPolicyVerifiers([]cryptoutil.Verifier{tt.verifier}), VerifyWithPolicyCARoots(tt.Roots), VerifyWithPolicyTimestampAuthorities(tv)) if tt.certConstraints != nil { - tt.certConstraints(&vo) + o = append(o, tt.certConstraints) } - err = verifyPolicySignature(context.TODO(), vo) - if err != nil && !tt.wantErr { - t.Errorf("testName = %s, error = %v, wantErr %v", tt.name, err, tt.wantErr) - } else { - fmt.Printf("test %s passed\n", tt.name) - } + vo := NewVerifyPolicySignatureOptions(o...) + err = VerifyPolicySignature(context.TODO(), env, vo) + assert.Equal(t, err != nil, tt.wantErr, "testName = %s, error = %v, wantErr = %v", tt.name, err, tt.wantErr) } } diff --git a/run.go b/run.go index e8f872db..63cfdfba 100644 --- a/run.go +++ b/run.go @@ -21,8 +21,6 @@ import ( "fmt" "github.com/in-toto/go-witness/attestation" - "github.com/in-toto/go-witness/attestation/environment" - "github.com/in-toto/go-witness/attestation/git" "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/dsse" "github.com/in-toto/go-witness/intoto" @@ -31,42 +29,65 @@ import ( type runOptions struct { stepName string - signer cryptoutil.Signer + signers []cryptoutil.Signer attestors []attestation.Attestor attestationOpts []attestation.AttestationContextOption timestampers []timestamp.Timestamper + insecure bool } type RunOption func(ro *runOptions) +// RunWithInsecure will allow attestations to be generated unsigned. If insecure is true, RunResult will not +// contain a signed DSSE envelope +func RunWithInsecure(insecure bool) RunOption { + return func(ro *runOptions) { + ro.insecure = insecure + } +} + +// RunWithAttestors defines which attestors should be run and added to the resulting AttestationCollection func RunWithAttestors(attestors []attestation.Attestor) RunOption { return func(ro *runOptions) { - ro.attestors = attestors + ro.attestors = append(ro.attestors, attestors...) } } +// RunWithAttestationOpts takes in any AttestationContextOptions and forwards them to the context that Run +// creates func RunWithAttestationOpts(opts ...attestation.AttestationContextOption) RunOption { return func(ro *runOptions) { ro.attestationOpts = opts } } +// RunWithTimestampers will timestamp any signatures created on the DSSE time envelope with the provided +// timestampers func RunWithTimestampers(ts ...timestamp.Timestamper) RunOption { return func(ro *runOptions) { ro.timestampers = ts } } +// RunWithSigners configures the signers that will be used to sign the DSSE envelope containing the generated +// attestation collection. +func RunWithSigners(signers ...cryptoutil.Signer) RunOption { + return func(ro *runOptions) { + ro.signers = append(ro.signers, signers...) + } +} + +// RunResult contains the generated attestation collection as well as the signed DSSE envelope, if one was +// created. type RunResult struct { Collection attestation.Collection SignedEnvelope dsse.Envelope } -func Run(stepName string, signer cryptoutil.Signer, opts ...RunOption) (RunResult, error) { +func Run(stepName string, opts ...RunOption) (RunResult, error) { ro := runOptions{ - stepName: stepName, - signer: signer, - attestors: []attestation.Attestor{environment.New(), git.New()}, + stepName: stepName, + insecure: false, } for _, opt := range opts { @@ -100,9 +121,12 @@ func Run(stepName string, signer cryptoutil.Signer, opts ...RunOption) (RunResul } result.Collection = attestation.NewCollection(ro.stepName, runCtx.CompletedAttestors()) - result.SignedEnvelope, err = signCollection(result.Collection, dsse.SignWithSigners(ro.signer), dsse.SignWithTimestampers(ro.timestampers...)) - if err != nil { - return result, fmt.Errorf("failed to sign collection: %w", err) + + if !ro.insecure { + result.SignedEnvelope, err = signCollection(result.Collection, dsse.SignWithSigners(ro.signers...), dsse.SignWithTimestampers(ro.timestampers...)) + if err != nil { + return result, fmt.Errorf("failed to sign collection: %w", err) + } } return result, nil @@ -113,8 +137,8 @@ func validateRunOpts(ro runOptions) error { return fmt.Errorf("step name is required") } - if ro.signer == nil { - return fmt.Errorf("signer is required") + if len(ro.signers) == 0 && !ro.insecure { + return fmt.Errorf("at lease one signer is required if not in insecure mode") } return nil diff --git a/slsa/verificationsummary.go b/slsa/verificationsummary.go new file mode 100644 index 00000000..bb1a8b07 --- /dev/null +++ b/slsa/verificationsummary.go @@ -0,0 +1,46 @@ +// 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 slsa + +import ( + "time" + + "github.com/in-toto/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"` + Policy ResourceDescriptor `json:"policy"` + InputAttestations []ResourceDescriptor `json:"inputAttestations"` + VerificationResult VerificationResult `json:"verificationResult"` +} diff --git a/verify.go b/verify.go index 3ba384e8..21f40002 100644 --- a/verify.go +++ b/verify.go @@ -16,18 +16,18 @@ package witness import ( "context" - "crypto/x509" - "encoding/base64" "encoding/json" "fmt" "io" + "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/attestation/policyverify" "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/dsse" - "github.com/in-toto/go-witness/log" + ipolicy "github.com/in-toto/go-witness/internal/policy" "github.com/in-toto/go-witness/policy" + "github.com/in-toto/go-witness/slsa" "github.com/in-toto/go-witness/source" - "github.com/in-toto/go-witness/timestamp" ) func VerifySignature(r io.Reader, verifiers ...cryptoutil.Verifier) (dsse.Envelope, error) { @@ -42,203 +42,115 @@ func VerifySignature(r io.Reader, verifiers ...cryptoutil.Verifier) (dsse.Envelo } type verifyOptions struct { - policyTimestampAuthorities []timestamp.TimestampVerifier - policyCARoots []*x509.Certificate - policyCAIntermediates []*x509.Certificate - policyCommonName string - policyDNSNames []string - policyEmails []string - policyOrganizations []string - policyURIs []string - policyEnvelope dsse.Envelope - policyVerifiers []cryptoutil.Verifier - collectionSource source.Sourcer - subjectDigests []string + attestorOptions []policyverify.Option + verifyPolicySignatureOptions []ipolicy.Option + runOptions []RunOption + signers []cryptoutil.Signer } type VerifyOption func(*verifyOptions) -func VerifyWithSubjectDigests(subjectDigests []cryptoutil.DigestSet) VerifyOption { +// VerifyWithSigners will configure the provided signers to be used to sign a DSSE envelope with the resulting +// policyverify attestor. See VerifyWithRunOptions for additional options. +func VerifyWithSigners(signers ...cryptoutil.Signer) VerifyOption { return func(vo *verifyOptions) { - for _, set := range subjectDigests { - for _, digest := range set { - vo.subjectDigests = append(vo.subjectDigests, digest) - } - } + vo.signers = append(vo.signers, signers...) } } -func VerifyWithCollectionSource(source source.Sourcer) VerifyOption { +// VerifyWithSubjectDigests configured the "seed" subject digests to start evaluating a policy. This is typically +// the digest of the software artifact or some other identifying digest. +func VerifyWithSubjectDigests(subjectDigests []cryptoutil.DigestSet) VerifyOption { return func(vo *verifyOptions) { - vo.collectionSource = source + vo.attestorOptions = append(vo.attestorOptions, policyverify.VerifyWithSubjectDigests(subjectDigests)) } } -func VerifyWithPolicyTimestampAuthorities(authorities []timestamp.TimestampVerifier) VerifyOption { +// VerifyWithCollectionSource configures the policy engine's sources for signed attestation collections. +// For example: disk or archivista are two typical sources. +func VerifyWithCollectionSource(source source.Sourcer) VerifyOption { return func(vo *verifyOptions) { - vo.policyTimestampAuthorities = authorities + vo.attestorOptions = append(vo.attestorOptions, policyverify.VerifyWithCollectionSource(source)) } } -func VerifyWithPolicyCARoots(roots []*x509.Certificate) VerifyOption { +// VerifyWithAttestorOptions forwards the provided options to the policyverify attestor. +func VerifyWithAttestorOptions(opts ...policyverify.Option) VerifyOption { return func(vo *verifyOptions) { - vo.policyCARoots = roots + vo.attestorOptions = append(vo.attestorOptions, opts...) } } -func VerifyWithPolicyCAIntermediates(intermediates []*x509.Certificate) VerifyOption { +// VerifyWithRunOptions forwards the provided RunOptions to the Run function that Verify calls. +func VerifyWithRunOptions(opts ...RunOption) VerifyOption { return func(vo *verifyOptions) { - vo.policyCAIntermediates = intermediates + vo.runOptions = append(vo.runOptions, opts...) } } func VerifyWithPolicyCertConstraints(commonName string, dnsNames []string, emails []string, organizations []string, uris []string) VerifyOption { return func(vo *verifyOptions) { - vo.policyCommonName = commonName - vo.policyDNSNames = dnsNames - vo.policyEmails = emails - vo.policyOrganizations = organizations - vo.policyURIs = uris + vo.verifyPolicySignatureOptions = append(vo.verifyPolicySignatureOptions, ipolicy.VerifyWithPolicyCertConstraints(commonName, dnsNames, emails, organizations, uris)) } } +type VerifyResult struct { + RunResult + VerificationSummary slsa.VerificationSummary + StepResults map[string]policy.StepResult +} + // 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]policy.StepResult, error) { - vo := verifyOptions{ - policyEnvelope: policyEnvelope, - policyVerifiers: policyVerifiers, - policyCommonName: "*", - policyDNSNames: []string{"*"}, - policyOrganizations: []string{"*"}, - policyURIs: []string{"*"}, - policyEmails: []string{"*"}, - } +func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers []cryptoutil.Verifier, opts ...VerifyOption) (VerifyResult, error) { + vo := verifyOptions{} for _, opt := range opts { opt(&vo) } - if err := verifyPolicySignature(ctx, vo); err != nil { - return nil, fmt.Errorf("failed to verify policy signature: %w", err) - } - - log.Info("policy signature verified") - - pol := policy.Policy{} - if err := json.Unmarshal(vo.policyEnvelope.Payload, &pol); err != nil { - return nil, fmt.Errorf("failed to parse policy: %w", err) - } - - pubKeysById, err := pol.PublicKeyVerifiers() - if err != nil { - return nil, 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 nil, 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, intermediates...) - } - - timestampAuthoritiesById, err := pol.TimestampAuthorityTrustBundles() - if err != nil { - return nil, fmt.Errorf("failed to load policy timestamp authorities: %w", err) - } - - timestampVerifiers := make([]timestamp.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.verifyPolicySignatureOptions = append(vo.verifyPolicySignatureOptions, ipolicy.VerifyWithPolicyVerifiers(policyVerifiers)) + vo.attestorOptions = append(vo.attestorOptions, policyverify.VerifyWithPolicyEnvelope(policyEnvelope), policyverify.VerifyWithPolicyVerificationOptions(vo.verifyPolicySignatureOptions...)) + if len(vo.signers) > 0 { + vo.runOptions = append(vo.runOptions, RunWithSigners(vo.signers...)) + } else { + vo.runOptions = append(vo.runOptions, RunWithInsecure(true)) } - 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{ + policyverify.New(vo.attestorOptions...), + }, + ), ) - pass, results, err := pol.Verify(ctx, policy.WithSubjectDigests(vo.subjectDigests), policy.WithVerifiedSource(verifiedSource)) + runResult, err := Run("policyverify", vo.runOptions...) if err != nil { - return nil, fmt.Errorf("error encountered during policy verification: %w", err) + return VerifyResult{}, err } - if !pass { - return results, fmt.Errorf("policy verification failed") + vr := VerifyResult{ + RunResult: runResult, } - return results, nil -} - -func verifyPolicySignature(ctx context.Context, vo verifyOptions) error { - passedPolicyVerifiers, err := vo.policyEnvelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...), dsse.VerifyWithTimestampVerifiers(vo.policyTimestampAuthorities...), dsse.VerifyWithRoots(vo.policyCARoots...), dsse.VerifyWithIntermediates(vo.policyCAIntermediates...)) - if err != nil { - return fmt.Errorf("could not verify policy: %w", err) - } - - var passed bool - for _, verifier := range passedPolicyVerifiers { - kid, err := verifier.Verifier.KeyID() - if err != nil { - return fmt.Errorf("could not get verifier key id: %w", err) - } - - var f policy.Functionary - trustBundle := make(map[string]policy.TrustBundle) - if _, ok := verifier.Verifier.(*cryptoutil.X509Verifier); ok { - rootIDs := make([]string, 0) - for _, root := range vo.policyCARoots { - id := base64.StdEncoding.EncodeToString(root.Raw) - rootIDs = append(rootIDs, id) - trustBundle[id] = policy.TrustBundle{ - Root: root, - } - } - - f = policy.Functionary{ - Type: "root", - CertConstraint: policy.CertConstraint{ - Roots: rootIDs, - CommonName: vo.policyCommonName, - URIs: vo.policyURIs, - Emails: vo.policyEmails, - Organizations: vo.policyOrganizations, - DNSNames: vo.policyDNSNames, - }, + for _, att := range runResult.Collection.Attestations { + if att.Type == slsa.VerificationSummaryPredicate { + verificationAttestor, ok := att.Attestation.(*policyverify.Attestor) + if !ok { + return VerifyResult{}, fmt.Errorf("unknown attestor %T", att.Attestation) } - } else { - f = policy.Functionary{ - Type: "key", - PublicKeyID: kid, - } - } - - err = f.Validate(verifier.Verifier, trustBundle) - if err != nil { - log.Debugf("Policy Verifier %s failed failed to match supplied constraints: %w, continuing...", kid, err) - continue + vr.StepResults = verificationAttestor.StepResults() + vr.VerificationSummary = verificationAttestor.VerificationSummary + break } - passed = true } - if !passed { - return fmt.Errorf("no policy verifiers passed verification") - } else { - return nil + if vr.VerificationSummary.VerificationResult != slsa.PassedVerificationResult { + return vr, fmt.Errorf("policy verification failed") } + + return vr, nil }