// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "bytes" "context" "encoding/json" "fmt" "strings" "testing" "github.com/hashicorp/vault/api" ) func TestPKIVerifySign(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) defer closer() // Relationship Map to Create // pki-root | pki-newroot | pki-empty // RootX1 RootX2 RootX4 RootX3 // | | // ---------------------------------------------- // v v // IntX1 IntX2 pki-int // | | // v v // IntX3 (-----------------------) IntX3 // // Here X1,X2 have the same name (same mount) // RootX4 uses the same key as RootX1 (but a different common_name/subject) // RootX3 has the same name, and is on a different mount // RootX1 has issued IntX1; RootX3 has issued IntX2 createComplicatedIssuerSetUp(t, client) runPkiVerifySignTests(t, client) } func runPkiVerifySignTests(t *testing.T, client *api.Client) { cases := []struct { name string args []string expectedMatches map[string]bool jsonOut bool shouldError bool expectErrorCont string expectErrorNotCont string nonJsonOutputCont string }{ { "rootX1-matches-rootX1", []string{"pki", "verify-sign", "-format=json", "pki-root/issuer/rootX1", "pki-root/issuer/rootX1"}, map[string]bool{ "key_id_match": true, "path_match": true, "signature_match": true, "subject_match": true, "trust_match": true, }, true, false, "", "", "", }, { "rootX1-on-rootX2-onlySameName", []string{"pki", "verify-sign", "-format=json", "pki-root/issuer/rootX1", "pki-root/issuer/rootX2"}, map[string]bool{ "key_id_match": false, "path_match": false, "signature_match": false, "subject_match": true, "trust_match": false, }, true, false, "", "", "", }, } for _, testCase := range cases { var errString string var results map[string]interface{} var stdOut string if testCase.jsonOut { results, errString = execPKIVerifyJson(t, client, false, testCase.shouldError, testCase.args) } else { stdOut, errString = execPKIVerifyNonJson(t, client, testCase.shouldError, testCase.args) } // Verify Error Behavior if testCase.shouldError { if errString == "" { t.Fatalf("Expected error in Testcase %s : no error produced, got results %s", testCase.name, results) } if testCase.expectErrorCont != "" && !strings.Contains(errString, testCase.expectErrorCont) { t.Fatalf("Expected error in Testcase %s to contain %s, but got error %s", testCase.name, testCase.expectErrorCont, errString) } if testCase.expectErrorNotCont != "" && strings.Contains(errString, testCase.expectErrorNotCont) { t.Fatalf("Expected error in Testcase %s to not contain %s, but got error %s", testCase.name, testCase.expectErrorNotCont, errString) } } else { if errString != "" { t.Fatalf("Error in Testcase %s : no error expected, but got error: %s", testCase.name, errString) } } // Verify Output if testCase.jsonOut { isMatch, errString := verifyExpectedJson(testCase.expectedMatches, results) if !isMatch { t.Fatalf("Expected Results for Testcase %s, do not match returned results %s", testCase.name, errString) } } else { if !strings.Contains(stdOut, testCase.nonJsonOutputCont) { t.Fatalf("Expected standard output for Testcase %s to contain %s, but got %s", testCase.name, testCase.nonJsonOutputCont, stdOut) } } } } func execPKIVerifyJson(t *testing.T, client *api.Client, expectErrorUnmarshalling bool, expectErrorOut bool, callArgs []string) (map[string]interface{}, string) { stdout, stderr := execPKIVerifyNonJson(t, client, expectErrorOut, callArgs) var results map[string]interface{} if err := json.Unmarshal([]byte(stdout), &results); err != nil && !expectErrorUnmarshalling { t.Fatalf("failed to decode json response : %v \n json: \n%v", err, stdout) } return results, stderr } func execPKIVerifyNonJson(t *testing.T, client *api.Client, expectErrorOut bool, callArgs []string) (string, string) { stdout := bytes.NewBuffer(nil) stderr := bytes.NewBuffer(nil) runOpts := &RunOptions{ Stdout: stdout, Stderr: stderr, Client: client, } code := RunCustom(callArgs, runOpts) if !expectErrorOut && code != 0 { t.Fatalf("running command `%v` unsuccessful (ret %v)\nerr: %v", strings.Join(callArgs, " "), code, stderr.String()) } t.Log(stdout.String() + stderr.String()) return stdout.String(), stderr.String() } func convertListOfInterfaceToString(list []interface{}, sep string) string { newList := make([]string, len(list)) for i, interfa := range list { newList[i] = interfa.(string) } return strings.Join(newList, sep) } func createComplicatedIssuerSetUp(t *testing.T, client *api.Client) { // Relationship Map to Create // pki-root | pki-newroot | pki-empty // RootX1 RootX2 RootX4 RootX3 // | | // ---------------------------------------------- // v v // IntX1 IntX2 pki-int // | | // v v // IntX3 (-----------------------) IntX3 // // Here X1,X2 have the same name (same mount) // RootX4 uses the same key as RootX1 (but a different common_name/subject) // RootX3 has the same name, and is on a different mount // RootX1 has issued IntX1; RootX3 has issued IntX2 if err := client.Sys().Mount("pki-root", &api.MountInput{ Type: "pki", Config: api.MountConfigInput{ MaxLeaseTTL: "36500d", }, }); err != nil { t.Fatalf("pki mount error: %#v", err) } if err := client.Sys().Mount("pki-newroot", &api.MountInput{ Type: "pki", Config: api.MountConfigInput{ MaxLeaseTTL: "36500d", }, }); err != nil { t.Fatalf("pki mount error: %#v", err) } if err := client.Sys().Mount("pki-int", &api.MountInput{ Type: "pki", Config: api.MountConfigInput{ MaxLeaseTTL: "36500d", }, }); err != nil { t.Fatalf("pki mount error: %#v", err) } // Used to check handling empty list responses: Not Used for Any Issuers / Certificates if err := client.Sys().Mount("pki-empty", &api.MountInput{ Type: "pki", Config: api.MountConfigInput{}, }); err != nil { t.Fatalf("pki mount error: %#v", err) } resp, err := client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ "key_type": "ec", "common_name": "Root X", "ttl": "3650d", "issuer_name": "rootX1", "key_name": "rootX1", }) if err != nil || resp == nil { t.Fatalf("failed to prime CA: %v", err) } resp, err = client.Logical().Write("pki-root/root/generate/internal", map[string]interface{}{ "key_type": "ec", "common_name": "Root X", "ttl": "3650d", "issuer_name": "rootX2", }) if err != nil || resp == nil { t.Fatalf("failed to prime CA: %v", err) } if resp, err := client.Logical().Write("pki-newroot/root/generate/internal", map[string]interface{}{ "key_type": "ec", "common_name": "Root X", "ttl": "3650d", "issuer_name": "rootX3", }); err != nil || resp == nil { t.Fatalf("failed to prime CA: %v", err) } if resp, err := client.Logical().Write("pki-root/root/generate/existing", map[string]interface{}{ "common_name": "Root X4", "ttl": "3650d", "issuer_name": "rootX4", "key_ref": "rootX1", }); err != nil || resp == nil { t.Fatalf("failed to prime CA: %v", err) } // Intermediate X1 int1CsrResp, err := client.Logical().Write("pki-int/intermediate/generate/internal", map[string]interface{}{ "key_type": "rsa", "common_name": "Int X1", "ttl": "3650d", }) if err != nil || int1CsrResp == nil { t.Fatalf("failed to generate CSR: %v", err) } int1KeyId, ok := int1CsrResp.Data["key_id"] if !ok { t.Fatalf("no key_id produced when generating csr, response %v", int1CsrResp.Data) } int1CsrRaw, ok := int1CsrResp.Data["csr"] if !ok { t.Fatalf("no csr produced when generating intermediate, resp: %v", int1CsrResp) } int1Csr := int1CsrRaw.(string) int1CertResp, err := client.Logical().Write("pki-root/issuer/rootX1/sign-intermediate", map[string]interface{}{ "csr": int1Csr, }) if err != nil || int1CertResp == nil { t.Fatalf("failed to sign CSR: %v", err) } int1CertChainRaw, ok := int1CertResp.Data["ca_chain"] if !ok { t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int1CertResp) } int1CertChain := convertListOfInterfaceToString(int1CertChainRaw.([]interface{}), "\n") importInt1Resp, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ "pem_bundle": int1CertChain, }) if err != nil || importInt1Resp == nil { t.Fatalf("failed to import certificate: %v", err) } importIssuerIdMap, ok := importInt1Resp.Data["mapping"] if !ok { t.Fatalf("no mapping data returned on issuer import: %v", importInt1Resp) } for key, value := range importIssuerIdMap.(map[string]interface{}) { if value != nil && len(value.(string)) > 0 { if value != int1KeyId { t.Fatalf("Expected exactly one key_match to %v, got multiple: %v", int1KeyId, importIssuerIdMap) } if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ "issuer_name": "intX1", }); err != nil || resp == nil { t.Fatalf("error naming issuer %v", err) } } else { if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ "issuer_name": "rootX1", }); err != nil || resp == nil { t.Fatalf("error naming issuer parent %v", err) } } } // Intermediate X2 int2CsrResp, err := client.Logical().Write("pki-int/intermediate/generate/internal", map[string]interface{}{ "key_type": "ec", "common_name": "Int X2", "ttl": "3650d", }) if err != nil || int2CsrResp == nil { t.Fatalf("failed to generate CSR: %v", err) } int2KeyId, ok := int2CsrResp.Data["key_id"] if !ok { t.Fatalf("no key material returned from producing csr, resp: %v", int2CsrResp) } int2CsrRaw, ok := int2CsrResp.Data["csr"] if !ok { t.Fatalf("no csr produced when generating intermediate, resp: %v", int2CsrResp) } int2Csr := int2CsrRaw.(string) int2CertResp, err := client.Logical().Write("pki-newroot/issuer/rootX3/sign-intermediate", map[string]interface{}{ "csr": int2Csr, }) if err != nil || int2CertResp == nil { t.Fatalf("failed to sign CSR: %v", err) } int2CertChainRaw, ok := int2CertResp.Data["ca_chain"] if !ok { t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int2CertResp) } int2CertChain := convertListOfInterfaceToString(int2CertChainRaw.([]interface{}), "\n") importInt2Resp, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ "pem_bundle": int2CertChain, }) if err != nil || importInt2Resp == nil { t.Fatalf("failed to import certificate: %v", err) } importIssuer2IdMap, ok := importInt2Resp.Data["mapping"] if !ok { t.Fatalf("no mapping data returned on issuer import: %v", importInt2Resp) } for key, value := range importIssuer2IdMap.(map[string]interface{}) { if value != nil && len(value.(string)) > 0 { if value != int2KeyId { t.Fatalf("unexpected key_match with ca_chain, expected only %v, got %v", int2KeyId, importIssuer2IdMap) } if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ "issuer_name": "intX2", }); err != nil || resp == nil { t.Fatalf("error naming issuer %v", err) } } else { if resp, err := client.Logical().Write("pki-int/issuer/"+key, map[string]interface{}{ "issuer_name": "rootX3", }); err != nil || resp == nil { t.Fatalf("error naming parent issuer %v", err) } } } // Intermediate X3 int3CsrResp, err := client.Logical().Write("pki-int/intermediate/generate/internal", map[string]interface{}{ "key_type": "rsa", "common_name": "Int X3", "ttl": "3650d", }) if err != nil || int3CsrResp == nil { t.Fatalf("failed to generate CSR: %v", err) } int3KeyId, ok := int3CsrResp.Data["key_id"] int3CsrRaw, ok := int3CsrResp.Data["csr"] if !ok { t.Fatalf("no csr produced when generating intermediate, resp: %v", int3CsrResp) } int3Csr := int3CsrRaw.(string) // sign by intX1 and import int3CertResp1, err := client.Logical().Write("pki-int/issuer/intX1/sign-intermediate", map[string]interface{}{ "csr": int3Csr, }) if err != nil || int3CertResp1 == nil { t.Fatalf("failed to sign CSR: %v", err) } int3CertChainRaw1, ok := int3CertResp1.Data["ca_chain"] if !ok { t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int3CertResp1) } int3CertChain1 := convertListOfInterfaceToString(int3CertChainRaw1.([]interface{}), "\n") importInt3Resp1, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ "pem_bundle": int3CertChain1, }) if err != nil || importInt3Resp1 == nil { t.Fatalf("failed to import certificate: %v", err) } importIssuer3IdMap1, ok := importInt3Resp1.Data["mapping"] if !ok { t.Fatalf("no mapping data returned on issuer import: %v", importInt2Resp) } for key, value := range importIssuer3IdMap1.(map[string]interface{}) { if value != nil && len(value.(string)) > 0 && value == int3KeyId { if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ "issuer_name": "intX3", }); err != nil || resp == nil { t.Fatalf("error naming issuer %v", err) } break } } // sign by intX2 and import int3CertResp2, err := client.Logical().Write("pki-int/issuer/intX2/sign-intermediate", map[string]interface{}{ "csr": int3Csr, }) if err != nil || int3CertResp2 == nil { t.Fatalf("failed to sign CSR: %v", err) } int3CertChainRaw2, ok := int3CertResp2.Data["ca_chain"] if !ok { t.Fatalf("no ca_chain produced when signing intermediate, resp: %v", int3CertResp2) } int3CertChain2 := convertListOfInterfaceToString(int3CertChainRaw2.([]interface{}), "\n") importInt3Resp2, err := client.Logical().Write("pki-int/issuers/import/cert", map[string]interface{}{ "pem_bundle": int3CertChain2, }) if err != nil || importInt3Resp2 == nil { t.Fatalf("failed to import certificate: %v", err) } importIssuer3IdMap2, ok := importInt3Resp2.Data["mapping"] if !ok { t.Fatalf("no mapping data returned on issuer import: %v", importInt2Resp) } for key, value := range importIssuer3IdMap2.(map[string]interface{}) { if value != nil && len(value.(string)) > 0 && value == int3KeyId { if resp, err := client.Logical().JSONMergePatch(context.Background(), "pki-int/issuer/"+key, map[string]interface{}{ "issuer_name": "intX3also", }); err != nil || resp == nil { t.Fatalf("error naming issuer %v", err) } break // Parent Certs Already Named } } } func verifyExpectedJson(expectedResults map[string]bool, results map[string]interface{}) (isMatch bool, error string) { if len(expectedResults) != len(results) { return false, fmt.Sprintf("Different Number of Keys in Expected Results (%d), than results (%d)", len(expectedResults), len(results)) } for key, value := range expectedResults { if results[key].(bool) != value { return false, fmt.Sprintf("Different value for key %s : expected %t got %s", key, value, results[key]) } } return true, "" }