diff --git a/internal/test/util.go b/internal/test/util.go new file mode 100644 index 00000000..bccdc6ff --- /dev/null +++ b/internal/test/util.go @@ -0,0 +1,145 @@ +// Copyright 2024 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 test + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" + + "github.com/in-toto/go-witness/cryptoutil" +) + +func CreateRsaKey() (*rsa.PrivateKey, *rsa.PublicKey, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + return priv, &priv.PublicKey, nil +} + +func CreateTestKey() (cryptoutil.Signer, cryptoutil.Verifier, []byte, error) { + privKey, _, err := CreateRsaKey() + if err != nil { + return nil, nil, nil, err + } + + signer := cryptoutil.NewRSASigner(privKey, crypto.SHA256) + verifier := cryptoutil.NewRSAVerifier(&privKey.PublicKey, crypto.SHA256) + keyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) + if err != nil { + return nil, nil, nil, err + } + + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: keyBytes}) + + return signer, verifier, pemBytes, nil +} + +func CreateCert(priv, pub interface{}, temp, parent *x509.Certificate) (*x509.Certificate, error) { + var err error + temp.SerialNumber, err = rand.Int(rand.Reader, big.NewInt(4294967295)) + if err != nil { + return nil, err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, temp, parent, pub, priv) + if err != nil { + return nil, err + } + + return x509.ParseCertificate(certBytes) +} + +func CreateRoot() (*x509.Certificate, interface{}, error) { + priv, pub, err := CreateRsaKey() + if err != nil { + return nil, nil, err + } + + template := &x509.Certificate{ + DNSNames: []string{"in-toto.io"}, + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"in-toto"}, + CommonName: "Test Root", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: false, + MaxPathLen: 2, + } + + cert, err := CreateCert(priv, pub, template, template) + return cert, priv, err +} + +func CreateIntermediate(parent *x509.Certificate, parentPriv interface{}) (*x509.Certificate, interface{}, error) { + priv, pub, err := CreateRsaKey() + if err != nil { + return nil, nil, err + } + + template := &x509.Certificate{ + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"TestifySec"}, + CommonName: "Test Intermediate", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: false, + MaxPathLen: 1, + } + + cert, err := CreateCert(parentPriv, pub, template, parent) + return cert, priv, err +} + +func CreateLeaf(parent *x509.Certificate, parentPriv interface{}) (*x509.Certificate, interface{}, error) { + priv, pub, err := CreateRsaKey() + if err != nil { + return nil, nil, err + } + + template := &x509.Certificate{ + DNSNames: []string{"in-toto.io"}, + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"In-toto"}, + CommonName: "Test Leaf", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: false, + } + + cert, err := CreateCert(parentPriv, pub, template, parent) + return cert, priv, err +} diff --git a/policy/policy.go b/policy/policy.go index 3eddcd94..799a2c0b 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -236,35 +236,13 @@ func (step Step) checkFunctionaries(verifiedStatements []source.VerifiedCollecti } for _, verifier := range verifiedStatement.Verifiers { - verifierID, err := verifier.KeyID() - if err != nil { - log.Debugf("(policy) skipping verifier: could not get key id: %w", err) - continue - } - for _, functionary := range step.Functionaries { - if functionary.PublicKeyID != "" && functionary.PublicKeyID == verifierID { - collections = append(collections, verifiedStatement) - break - } - - x509Verifier, ok := verifier.(*cryptoutil.X509Verifier) - if !ok { - log.Debugf("(policy) skipping verifier: verifier with ID %v is not a public key verifier or a x509 verifier", verifierID) - continue - } - - if len(functionary.CertConstraint.Roots) == 0 { - log.Debugf("(policy) skipping verifier: verifier with ID %v is an x509 verifier, but step %v does not have any truested roots", verifierID, step) - continue - } - - if err := functionary.CertConstraint.Check(x509Verifier, trustBundles); err != nil { - log.Debugf("(policy) skipping verifier: verifier with ID %v doesn't meet certificate constraint: %w", verifierID, err) + if err := functionary.Validate(verifier, trustBundles); err != nil { + log.Debugf("(policy) skipping verifier: %w", err) continue + } else { + collections = append(collections, verifiedStatement) } - - collections = append(collections, verifiedStatement) } } } diff --git a/policy/step.go b/policy/step.go index b1b63ef0..ea451bf8 100644 --- a/policy/step.go +++ b/policy/step.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/in-toto/go-witness/attestation" + "github.com/in-toto/go-witness/cryptoutil" "github.com/in-toto/go-witness/source" ) @@ -80,6 +81,32 @@ type RejectedCollection struct { Reason error } +func (f Functionary) Validate(verifier cryptoutil.Verifier, trustBundles map[string]TrustBundle) error { + verifierID, err := verifier.KeyID() + if err != nil { + return fmt.Errorf("could not get key id: %w", err) + } + + if f.PublicKeyID != "" && f.PublicKeyID == verifierID { + return nil + } + + x509Verifier, ok := verifier.(*cryptoutil.X509Verifier) + if !ok { + return fmt.Errorf("verifier with ID %v is not a public key verifier or a x509 verifier", verifierID) + } + + if len(f.CertConstraint.Roots) == 0 { + return fmt.Errorf("verifier with ID %v is an x509 verifier, but no trusted roots provided in functionary", verifierID) + } + + if err := f.CertConstraint.Check(x509Verifier, trustBundles); err != nil { + return fmt.Errorf("verifier with ID %v doesn't meet certificate constraint: %w", verifierID, err) + } + + return nil +} + // validateAttestations will test each collection against to ensure the expected attestations // appear in the collection as well as that any rego policies pass for the step. func (s Step) validateAttestations(verifiedCollections []source.VerifiedCollection) StepResult { diff --git a/verify.go b/verify.go index a0d2bdab..2aa4c74f 100644 --- a/verify.go +++ b/verify.go @@ -17,12 +17,14 @@ package witness import ( "context" "crypto/x509" + "encoding/base64" "encoding/json" "fmt" "io" "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/source" "github.com/in-toto/go-witness/timestamp" @@ -43,6 +45,11 @@ 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 @@ -85,22 +92,39 @@ func VerifyWithPolicyCAIntermediates(intermediates []*x509.Certificate) VerifyOp } } +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 + } +} + // 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, + policyEnvelope: policyEnvelope, + policyVerifiers: policyVerifiers, + policyCommonName: "*", + policyDNSNames: []string{"*"}, + policyOrganizations: []string{"*"}, + policyURIs: []string{"*"}, + policyEmails: []string{"*"}, } for _, opt := range opts { opt(&vo) } - if _, err := vo.policyEnvelope.Verify(dsse.VerifyWithVerifiers(vo.policyVerifiers...), dsse.VerifyWithTimestampVerifiers(vo.policyTimestampAuthorities...), dsse.VerifyWithRoots(vo.policyCARoots...), dsse.VerifyWithIntermediates(vo.policyCAIntermediates...)); err != nil { - return nil, fmt.Errorf("could not verify policy: %w", err) + if err := verifyPolicySignature(ctx, vo); err != nil { + return nil, fmt.Errorf("failed to verify policy signature: %w", err) } + log.Info("Policy signature verification passed") + 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) @@ -154,3 +178,62 @@ func Verify(ctx context.Context, policyEnvelope dsse.Envelope, policyVerifiers [ return accepted, 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, + }, + } + + } 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/verify_test.go new file mode 100644 index 00000000..cfb32d5d --- /dev/null +++ b/verify_test.go @@ -0,0 +1,162 @@ +// Copyright 2024 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 witness + +import ( + "bytes" + "context" + "crypto/x509" + "fmt" + "testing" + "time" + + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/dsse" + "github.com/in-toto/go-witness/internal/test" + "github.com/in-toto/go-witness/intoto" + "github.com/in-toto/go-witness/timestamp" +) + +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) + } + + badRootCert, _, err := test.CreateRoot() + if err != nil { + t.Fatal(err) + } + + rootCert, key, err := test.CreateRoot() + if err != nil { + t.Fatal(err) + } + + leafCert, leafPriv, err := test.CreateLeaf(rootCert, key) + if err != nil { + t.Fatal(err) + } + + x509Signer, err := cryptoutil.NewSigner(leafPriv, cryptoutil.SignWithCertificate(leafCert)) + if err != nil { + t.Fatal(err) + } + + timestampers := []timestamp.FakeTimestamper{ + {T: time.Now()}, + {T: time.Now().Add(12 * time.Hour)}, + } + + // Define the test cases. + tests := []struct { + name string + signer cryptoutil.Signer + verifier cryptoutil.Verifier + timestampers []timestamp.FakeTimestamper + Roots []*x509.Certificate + Intermediates []*x509.Certificate + certConstraints VerifyOption + wantErr bool + }{ + { + name: "valid rsa signature", + signer: rsaSigner, + verifier: rsaVerifier, + // passing in timestampers to ensure that it is ignored + timestampers: timestampers, + wantErr: false, + }, + { + name: "invalid rsa signature", + signer: rsaSigner, + Roots: []*x509.Certificate{rootCert}, + wantErr: true, + }, + { + name: "valid x509 signature", + signer: x509Signer, + // We're going to pass in to ensure that it is ignored + Roots: []*x509.Certificate{rootCert}, + wantErr: false, + }, + { + name: "valid x509 signature w/ constraints", + signer: x509Signer, + // We're going to pass in to ensure that it is ignored + Roots: []*x509.Certificate{rootCert}, + certConstraints: VerifyWithPolicyCertConstraints(leafCert.Subject.CommonName, leafCert.DNSNames, []string{"*"}, []string{"*"}, []string{"*"}), + timestampers: timestampers, + wantErr: false, + }, + { + name: "valid x509 signature w/ bad constraints", + signer: x509Signer, + // We're going to pass in to ensure that it is ignored + Roots: []*x509.Certificate{rootCert}, + certConstraints: VerifyWithPolicyCertConstraints("foo", []string{"bar"}, []string{"baz"}, []string{"qux"}, []string{"quux"}), + wantErr: true, + }, + { + name: "unknown root", + signer: x509Signer, + // We're going to pass in to ensure that it is ignored + Roots: []*x509.Certificate{badRootCert}, + wantErr: true, + }, + } + + for _, tt := range tests { + var ts []timestamp.Timestamper + for _, t := range tt.timestampers { + ts = append(ts, 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) + } + + 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{"*"}, + } + + if tt.certConstraints != nil { + tt.certConstraints(&vo) + } + + 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) + } + + } +}