From 4ab0f683f6b494e5a195acaee4d07ee90f553e52 Mon Sep 17 00:00:00 2001 From: Cole Kennedy Date: Tue, 22 Mar 2022 12:28:15 -0500 Subject: [PATCH] CHORE: add testing to cmd package --- cmd/witness/cmd/root_test.go | 249 ++++++++++++++++++++++++++++++++ cmd/witness/cmd/run_test.go | 189 +++++++++++++++++++++++++ cmd/witness/cmd/sign_test.go | 61 ++++++++ cmd/witness/cmd/verify_test.go | 251 +++++++++++++++++++++++++++++++++ 4 files changed, 750 insertions(+) create mode 100644 cmd/witness/cmd/root_test.go create mode 100644 cmd/witness/cmd/run_test.go create mode 100644 cmd/witness/cmd/sign_test.go create mode 100644 cmd/witness/cmd/verify_test.go diff --git a/cmd/witness/cmd/root_test.go b/cmd/witness/cmd/root_test.go new file mode 100644 index 00000000..86add6e4 --- /dev/null +++ b/cmd/witness/cmd/root_test.go @@ -0,0 +1,249 @@ +// Copyright 2022 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 cmd + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "testing" + "time" + + "github.com/testifysec/witness/cmd/witness/options" +) + +const ( + keybits = 512 +) + +func Test_loadSignersKeyPair(t *testing.T) { + privatePem, _ := rsakeypair(t) + + keyOptions := options.KeyOptions{ + KeyPath: privatePem.Name(), + } + + _, errors := loadSigners(context.Background(), keyOptions) + if len(errors) != 0 { + t.Errorf("unexpected errors: %v", errors) + } + + keyOptions.KeyPath = "not-a-file" + _, errors = loadSigners(context.Background(), keyOptions) + if len(errors) != 1 { + t.Errorf("expected 1 error, got %d", len(errors)) + } +} + +func Test_loadSignersCertificate(t *testing.T) { + _, intermediates, leafcert, leafkey := fullChain(t) + + keyOptions := options.KeyOptions{ + KeyPath: leafkey.Name(), + IntermediatePaths: []string{ + intermediates[0].Name(), + }, + CertPath: leafcert.Name(), + } + + signers, errors := loadSigners(context.Background(), keyOptions) + if len(errors) != 0 { + t.Errorf("unexpected errors: %v", errors) + } + + _, err := signers[0].Verifier() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(signers) != 1 { + t.Errorf("expected 1 signer, got %d", len(signers)) + } +} + +func rsakeypair(t *testing.T) (privatePem *os.File, publicPem *os.File) { + privatekey, err := rsa.GenerateKey(rand.Reader, keybits) + if err != nil { + t.Fatal(err) + } + + publickey := &privatekey.PublicKey + + privatekey_bytes := x509.MarshalPKCS1PrivateKey(privatekey) + + privateKeyBlock := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: privatekey_bytes, + } + + workingDir := t.TempDir() + + privatePem, err = os.CreateTemp(workingDir, "key.pem") + if err != nil { + t.Fatal(err) + } + + err = pem.Encode(privatePem, privateKeyBlock) + if err != nil { + t.Fatal(err) + } + + publicKeyBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(publickey), + } + + publicPem, err = os.CreateTemp(workingDir, "key.pub") + if err != nil { + t.Fatal(err) + } + + err = pem.Encode(publicPem, publicKeyBlock) + if err != nil { + t.Fatal(err) + } + + return privatePem, publicPem + +} + +//ref: https://jamielinux.com/docs/openssl-certificate-authority/appendix/intermediate-configuration-file.html +func fullChain(t *testing.T) (caPem *os.File, intermediatePems []*os.File, leafPem *os.File, leafkeyPem *os.File) { + workingDir := t.TempDir() + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(42), + Subject: pkix.Name{ + Organization: []string{"Witness Testing"}, + CommonName: "Witness Testing CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + caPrivateKey, err := rsa.GenerateKey(rand.Reader, keybits) + if err != nil { + t.Fatal(err) + } + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivateKey.PublicKey, caPrivateKey) + if err != nil { + t.Fatal(err) + } + + caPem, err = os.CreateTemp(workingDir, "ca.pem") + if err != nil { + t.Fatal(err) + } + + err = pem.Encode(caPem, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}) + if err != nil { + t.Fatal(err) + } + + //common name must be different than the CA name + intermediate := &x509.Certificate{ + SerialNumber: big.NewInt(43), + Subject: pkix.Name{ + Organization: []string{"Witness Testing"}, + CommonName: "Witness Testing Intermediate CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + intermediatePrivateKey, err := rsa.GenerateKey(rand.Reader, keybits) + if err != nil { + t.Fatal(err) + } + + intermediateCertBytes, err := x509.CreateCertificate(rand.Reader, intermediate, ca, &intermediatePrivateKey.PublicKey, caPrivateKey) + if err != nil { + t.Fatal(err) + } + + intermediatePem, err := os.CreateTemp(workingDir, "intermediate.pem") + if err != nil { + t.Fatal(err) + } + + err = pem.Encode(intermediatePem, &pem.Block{Type: "CERTIFICATE", Bytes: intermediateCertBytes}) + if err != nil { + t.Fatal(err) + } + + intermediatePems = []*os.File{intermediatePem} + + leaf := &x509.Certificate{ + SerialNumber: big.NewInt(44), + Subject: pkix.Name{ + Organization: []string{"Witness Testing"}, + CommonName: "Witness Testing Leaf", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: false, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + KeyUsage: x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + + leafPrivateKey, err := rsa.GenerateKey(rand.Reader, keybits) + if err != nil { + t.Fatal(err) + } + + leafCertBytes, err := x509.CreateCertificate(rand.Reader, leaf, intermediate, &leafPrivateKey.PublicKey, intermediatePrivateKey) + if err != nil { + t.Fatal(err) + } + + leafPem, err = os.CreateTemp(workingDir, "leaf.pem") + if err != nil { + t.Fatal(err) + } + + err = pem.Encode(leafPem, &pem.Block{Type: "CERTIFICATE", Bytes: leafCertBytes}) + if err != nil { + t.Fatal(err) + } + + leafkeyPem, err = os.CreateTemp(workingDir, "leaf.key") + + if err != nil { + t.Fatal(err) + } + + err = pem.Encode(leafkeyPem, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafPrivateKey)}) + if err != nil { + t.Fatal(err) + } + + return caPem, intermediatePems, leafPem, leafkeyPem + +} diff --git a/cmd/witness/cmd/run_test.go b/cmd/witness/cmd/run_test.go new file mode 100644 index 00000000..ca71ce51 --- /dev/null +++ b/cmd/witness/cmd/run_test.go @@ -0,0 +1,189 @@ +// Copyright 2021 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 cmd + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "testing" + + "github.com/testifysec/witness/cmd/witness/options" + "github.com/testifysec/witness/pkg/cryptoutil" +) + +func Test_runRunRSAKeyPair(t *testing.T) { + + priv, _ := rsakeypair(t) + keyOptions := options.KeyOptions{ + KeyPath: priv.Name(), + } + + workingDir := t.TempDir() + + runOptions := options.RunOptions{ + KeyOptions: keyOptions, + WorkingDir: workingDir, + Attestations: []string{}, + OutFilePath: workingDir + "outfile.txt", + StepName: "teststep", + RekorServer: "", + Tracing: false, + } + + args := []string{ + "bash", + "-c", + "echo 'test' > test.txt", + } + + err := runRun(runOptions, args) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + attestationBytes, err := os.ReadFile(workingDir + "outfile.txt") + if err != nil { + t.Error(err) + } + + if len(attestationBytes) < 1 { + t.Errorf("Unexpected output size") + } + + envelopePaths := []string{ + workingDir + "outfile.txt", + } + + envelopes, err := loadEnvelopesFromDisk(envelopePaths) + if err != nil { + t.Errorf("Error loading envelopes from disk: err: %v", err) + } + + if len(envelopes) != 1 { + t.Errorf("wrong number of envelopes") + } +} + +func Test_runRunRSACA(t *testing.T) { + + _, intermediates, leafcert, leafkey := fullChain(t) + + workingDir := t.TempDir() + + intermediateNames := []string{} + for _, intermediate := range intermediates { + intermediateNames = append(intermediateNames, intermediate.Name()) + } + + keyOptions := options.KeyOptions{ + KeyPath: leafkey.Name(), + CertPath: leafcert.Name(), + IntermediatePaths: intermediateNames, + SpiffePath: "", + } + + runOptions := options.RunOptions{ + KeyOptions: keyOptions, + WorkingDir: workingDir, + Attestations: []string{}, + OutFilePath: workingDir + "outfile.txt", + StepName: "teststep", + RekorServer: "", + Tracing: false, + } + + args := []string{ + "bash", + "-c", + "echo 'test' > test.txt", + } + + err := runRun(runOptions, args) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + attestationBytes, err := os.ReadFile(workingDir + "outfile.txt") + if err != nil { + t.Error(err) + } + + if len(attestationBytes) < 1 { + t.Errorf("Unexpected output size") + } + + envelopePaths := []string{ + workingDir + "outfile.txt", + } + + envelopes, err := loadEnvelopesFromDisk(envelopePaths) + if err != nil { + t.Errorf("Error loading envelopes from disk: err: %v", err) + } + + if len(envelopes) != 1 { + t.Errorf("wrong number of envelopes") + } + + b, err := os.ReadFile(intermediateNames[0]) + if err != nil { + t.Errorf("Error reading intermediate cert: %v", err) + } + + if !bytes.Equal(b, envelopes[0].Signatures[0].Intermediates[0]) { + t.Errorf("Intermediates do not match") + } + + b, err = os.ReadFile(leafcert.Name()) + if err != nil { + t.Errorf("Error reading leaf cert: %v", err) + } + + if !bytes.Equal(b, envelopes[0].Signatures[0].Certificate) { + t.Errorf("Leaf cert does not match") + } + +} + +func createTestRSAKey() (cryptoutil.Signer, cryptoutil.Verifier, []byte, []byte, error) { + privKey, err := rsa.GenerateKey(rand.Reader, keybits) + if err != nil { + return nil, 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, nil, err + } + + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: keyBytes}) + if err != nil { + return nil, nil, nil, nil, err + } + + privKeyBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privKey)}) + if err != nil { + return nil, nil, nil, nil, err + } + + return signer, verifier, pemBytes, privKeyBytes, nil +} diff --git a/cmd/witness/cmd/sign_test.go b/cmd/witness/cmd/sign_test.go new file mode 100644 index 00000000..4e6b771e --- /dev/null +++ b/cmd/witness/cmd/sign_test.go @@ -0,0 +1,61 @@ +// Copyright 2021 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 cmd + +import ( + "os" + "testing" + + "github.com/testifysec/witness/cmd/witness/options" +) + +func Test_runSignPolicyRSA(t *testing.T) { + priv, _ := rsakeypair(t) + + keyOptions := options.KeyOptions{ + KeyPath: priv.Name(), + } + + workingDir := t.TempDir() + + testdata := []byte("test") + + err := os.WriteFile(workingDir+"test.txt", testdata, 0644) + if err != nil { + t.Error(err) + } + + signOptions := options.SignOptions{ + KeyOptions: keyOptions, + DataType: "text", + OutFilePath: workingDir + "outfile.txt", + InFilePath: workingDir + "test.txt", + } + + err = runSign(signOptions) + if err != nil { + t.Error(err) + } + + signedBytes, err := os.ReadFile(workingDir + "outfile.txt") + if err != nil { + t.Error(err) + } + + if len(signedBytes) < 1 { + t.Errorf("Unexpected output size") + } + +} diff --git a/cmd/witness/cmd/verify_test.go b/cmd/witness/cmd/verify_test.go new file mode 100644 index 00000000..e4909ff3 --- /dev/null +++ b/cmd/witness/cmd/verify_test.go @@ -0,0 +1,251 @@ +// Copyright 2021 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 cmd + +import ( + "bytes" + "encoding/json" + "os" + "testing" + "time" + + "github.com/testifysec/witness/cmd/witness/options" + witness "github.com/testifysec/witness/pkg" + "github.com/testifysec/witness/pkg/attestation/commandrun" + "github.com/testifysec/witness/pkg/dsse" + "github.com/testifysec/witness/pkg/policy" +) + +func Test_loadEnvelopesFromDisk(t *testing.T) { + testPayload := []byte("test") + + envelope := dsse.Envelope{ + Payload: testPayload, + PayloadType: "text", + Signatures: []dsse.Signature{}, + } + + jsonEnvelope, err := json.Marshal(envelope) + if err != nil { + t.Error(err) + } + + workingDir := t.TempDir() + + err = os.MkdirAll(workingDir, 0755) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(workingDir+"envelope.txt", jsonEnvelope, 0644) + + if err != nil { + t.Error(err) + } + + envelopes, err := loadEnvelopesFromDisk([]string{workingDir + "envelope.txt"}) + if err != nil { + t.Error(err) + } + + if len(envelopes) != 1 { + t.Errorf("expected 1 envelope, got %d", len(envelopes)) + } + + if string(envelopes[0].Payload) != string(testPayload) { + t.Errorf("expected payload to be %s, got %s", string(testPayload), string(envelopes[0].Payload)) + } + + if envelopes[0].PayloadType != "text" { + t.Errorf("expected payload type to be text, got %s", envelopes[0].PayloadType) + } + + if len(envelopes[0].Signatures) != 0 { + t.Errorf("expected 0 signatures, got %d", len(envelopes[0].Signatures)) + } + + err = os.RemoveAll("/tmp/witness") + if err != nil { + t.Error(err) + } + +} + +func Test_RunVerifyKeyPair(t *testing.T) { + policy, funcPriv := makepolicyRSAPub(t) + signedPolicy, pub := signPolicy(t, policy) + + workingDir := t.TempDir() + attestationDir := t.TempDir() + + err := os.WriteFile(workingDir+"signed-policy.json", signedPolicy, 0644) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(workingDir+"policy-pub.pem", pub, 0644) + if err != nil { + t.Error(err) + } + + err = os.WriteFile(workingDir+"func-priv.pem", funcPriv, 0644) + if err != nil { + t.Error(err) + } + + keyOptions := options.KeyOptions{ + KeyPath: workingDir + "func-priv.pem", + } + + step1Args := []string{ + "bash", + "-c", + "echo 'test01' > test.txt", + } + + s1RunOptions := options.RunOptions{ + KeyOptions: keyOptions, + WorkingDir: workingDir, + Attestations: []string{}, + OutFilePath: attestationDir + "step01.json", + StepName: "step01", + RekorServer: "", + Tracing: false, + } + + err = runRun(s1RunOptions, step1Args) + if err != nil { + t.Error(err) + } + + step2Args := []string{ + "bash", + "-c", + "echo 'test02' >> test.txt", + } + + s2RunOptions := options.RunOptions{ + KeyOptions: keyOptions, + WorkingDir: workingDir, + Attestations: []string{}, + OutFilePath: attestationDir + "step02.json", + StepName: "step02", + RekorServer: "", + Tracing: false, + } + + err = runRun(s2RunOptions, step2Args) + if err != nil { + t.Error(err) + } + + vo := options.VerifyOptions{ + KeyPath: workingDir + "policy-pub.pem", + AttestationFilePaths: []string{attestationDir + "step01.json", attestationDir + "step02.json"}, + PolicyFilePath: workingDir + "signed-policy.json", + ArtifactFilePath: workingDir + "test.txt", + RekorServer: "", + } + + err = runVerify(vo, []string{}) + if err != nil { + t.Error(err) + } + +} + +func signPolicy(t *testing.T, p []byte) (signedPolicy []byte, pub []byte) { + sign, _, pub, _, err := createTestRSAKey() + if err != nil { + t.Error(err) + } + + reader := bytes.NewReader(p) + outBytes := []byte{} + + writer := bytes.NewBuffer(outBytes) + + err = witness.Sign(reader, "https://witness.testifysec.com/policy/v0.1", writer, sign) + if err != nil { + t.Error(err) + } + + return writer.Bytes(), pub +} + +func makepolicyRSAPub(t *testing.T) ([]byte, []byte) { + _, ver, pub, fpriv, err := createTestRSAKey() + if err != nil { + t.Error(err) + } + + keyID, err := ver.KeyID() + if err != nil { + t.Error(err) + } + + functionary := policy.Functionary{ + Type: "PublicKey", + PublicKeyID: keyID, + } + + pk := policy.PublicKey{ + KeyID: keyID, + Key: pub, + } + + root := policy.Root{} + + p := makepolicy(t, functionary, pk, root) + return p, fpriv +} + +func makepolicy(t *testing.T, functionary policy.Functionary, publicKey policy.PublicKey, root policy.Root) []byte { + step01 := policy.Step{ + Name: "step01", + Functionaries: []policy.Functionary{functionary}, + Attestations: []policy.Attestation{{Type: commandrun.Type}}, + } + + step02 := policy.Step{ + Name: "step02", + Functionaries: []policy.Functionary{functionary}, + Attestations: []policy.Attestation{{Type: commandrun.Type}}, + ArtifactsFrom: []string{"step01"}, + } + + p := policy.Policy{ + Expires: time.Now().Add(1 * time.Hour), + PublicKeys: map[string]policy.PublicKey{}, + Steps: map[string]policy.Step{}, + } + + if functionary.CertConstraint.Roots != nil { + keyID := functionary.CertConstraint.Roots[0] + p.Roots[keyID] = root + } + + p.Steps = make(map[string]policy.Step) + p.Steps[step01.Name] = step01 + p.Steps[step02.Name] = step02 + + p.PublicKeys[publicKey.KeyID] = publicKey + pb, err := json.MarshalIndent(p, "", " ") + if err != nil { + t.Error(err) + } + + return pb +}