diff --git a/go.mod b/go.mod index f5aebca4..87cbdbd7 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/hashicorp/consul/api v1.1.0 // indirect github.com/hashicorp/go-gcp-common v0.5.0 // indirect - github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f // indirect + github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f github.com/hashicorp/go-memdb v0.0.0-20181108192425-032f93b25bec // indirect github.com/hashicorp/go-plugin v1.0.1-0.20190509212451-a1756f37cec6 // indirect github.com/hashicorp/go-retryablehttp v0.5.0 // indirect diff --git a/plugin/pki/backend_test.go b/plugin/pki/backend_test.go index 7bbb5cd1..9a5f4c0c 100644 --- a/plugin/pki/backend_test.go +++ b/plugin/pki/backend_test.go @@ -61,7 +61,6 @@ func TestFakeStoreByOptions(t *testing.T) { t.Run("read certificate by cn", integrationTestEnv.FakeReadCertificateByCN) t.Run("delete role", integrationTestEnv.DeleteRole) - //test store_by default t.Run("create role store_by serial", integrationTestEnv.FakeCreateRoleStoreBySerial) t.Run("issue", integrationTestEnv.FakeIssueCertificateAndSaveSerial) @@ -81,7 +80,6 @@ func TestFakeStoreByOptions(t *testing.T) { t.Run("delete role", integrationTestEnv.DeleteRole) } - //Testing Venafi Platform integration func TestTPPIntegration(t *testing.T) { diff --git a/plugin/pki/env_test.go b/plugin/pki/env_test.go index 18e40787..af2e196d 100644 --- a/plugin/pki/env_test.go +++ b/plugin/pki/env_test.go @@ -197,6 +197,12 @@ func (e *testEnv) failToWriteRoleToBackend(t *testing.T, configString venafiConf if resp != nil && !resp.IsError() { t.Fatal("Role with mixed cloud api key and tpp url should fail to write") } + + errText := resp.Data["error"].(string) + + if errText != errorTextTPPandCloudMixedCredentials { + t.Fatalf("Expecting error with text %s but got %s", errorTextTPPandCloudMixedCredentials, errText) + } } func (e *testEnv) listRolesInBackend(t *testing.T) { @@ -460,7 +466,7 @@ func (e *testEnv) CheckThatThereIsNoCertificate(t *testing.T, certId string) { } const noCertError = "no entry found in path" - certContain := strings.Contains(err.Error(),noCertError) + certContain := strings.Contains(err.Error(), noCertError) if !certContain { t.Fatalf("error should contain %s substring but it is %s", noCertError, err) } diff --git a/plugin/pki/path_roles.go b/plugin/pki/path_roles.go index be230dcd..7f7930bf 100644 --- a/plugin/pki/path_roles.go +++ b/plugin/pki/path_roles.go @@ -165,8 +165,15 @@ attached to them. Defaults to "false".`, } const ( - storeByCNString string = "cn" - storeBySerialString string = "serial" + storeByCNString = "cn" + storeBySerialString = "serial" + errorTextInvalidMode = "Invalid mode. fakemode or apikey or tpp credentials required" + errorTextValueMustBeLess = `"ttl" value must be less than "max_ttl" value` + errorTextTPPandCloudMixedCredentials = `TPP credentials and Cloud API key can't be specified in one role` + errorTextStoreByAndStoreByCNOrSerialConflict = `Can't specify both story_by and store_by_cn or store_by_serial options '` + errorTextNoStoreAndStoreByCNOrSerialConflict = `Can't specify both no_store and store_by_cn or store_by_serial options '` + errorTextNoStoreAndStoreByConflict = `Can't specify both no_store and store_by options '` + errTextStoreByWrongOption = "Option store_by can be %s or %s, not %s" ) func (b *backend) getRole(ctx context.Context, s logical.Storage, n string) (*roleEntry, error) { @@ -252,44 +259,60 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data GenerateLease: data.Get("generate_lease").(bool), ServerTimeout: time.Duration(data.Get("server_timeout").(int)) * time.Second, } + + err = validateEntry(entry) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + // Store it + jsonEntry, err := logical.StorageEntryJSON("role/"+name, entry) + if err != nil { + return nil, err + } + if err := req.Storage.Put(ctx, jsonEntry); err != nil { + return nil, err + } + + return nil, nil +} + +func validateEntry(entry *roleEntry) (err error) { if !entry.Fakemode && entry.Apikey == "" && (entry.TPPURL == "" || entry.TPPUser == "" || entry.TPPPassword == "") { - return logical.ErrorResponse("Invalid mode. fakemode or apikey or tpp credentials required"), nil + return fmt.Errorf(errorTextInvalidMode) } + if entry.MaxTTL > 0 && entry.TTL > entry.MaxTTL { - return logical.ErrorResponse( - `"ttl" value must be less than "max_ttl" value`, - ), nil + return fmt.Errorf( + errorTextValueMustBeLess, + ) } if entry.TPPURL != "" && entry.Apikey != "" { - return logical.ErrorResponse( - `TPP url and Cloud API key can't be specified in one role`, - ), nil + return fmt.Errorf(errorTextTPPandCloudMixedCredentials) + } + + if entry.TPPUser != "" && entry.Apikey != "" { + return fmt.Errorf(errorTextTPPandCloudMixedCredentials) } if (entry.StoreByCN || entry.StoreBySerial) && entry.StoreBy != "" { - return logical.ErrorResponse( - `Can't specify both story_by and store_by_cn or store_by_serial options '`, - ), nil + return fmt.Errorf(errorTextStoreByAndStoreByCNOrSerialConflict) } if (entry.StoreByCN || entry.StoreBySerial) && entry.NoStore { - return logical.ErrorResponse( - `Can't specify both no_store and store_by_cn or store_by_serial options '`, - ), nil + return fmt.Errorf(errorTextNoStoreAndStoreByCNOrSerialConflict) } if entry.StoreBy != "" && entry.NoStore { - return logical.ErrorResponse( - `Can't specify both no_store and store_by options '`, - ), nil + return fmt.Errorf(errorTextNoStoreAndStoreByConflict) } if entry.StoreBy != "" { if (entry.StoreBy != storeBySerialString) && (entry.StoreBy != storeByCNString) { - return logical.ErrorResponse( - fmt.Sprintf("Option store_by can be %s or %s, not %s", storeBySerialString, storeByCNString, entry.StoreBy), - ), nil + return fmt.Errorf( + fmt.Sprintf(errTextStoreByWrongOption, storeBySerialString, storeByCNString, entry.StoreBy), + ) } } @@ -302,16 +325,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data entry.StoreBy = storeByCNString } - // Store it - jsonEntry, err := logical.StorageEntryJSON("role/"+name, entry) - if err != nil { - return nil, err - } - if err := req.Storage.Put(ctx, jsonEntry); err != nil { - return nil, err - } - - return nil, nil + return nil } type roleEntry struct { diff --git a/plugin/pki/path_roles_test.go b/plugin/pki/path_roles_test.go new file mode 100644 index 00000000..31210822 --- /dev/null +++ b/plugin/pki/path_roles_test.go @@ -0,0 +1,161 @@ +package pki + +import ( + "fmt" + "testing" +) + +func TestRoleValidate(t *testing.T) { + + entry := &roleEntry{ + TPPURL: "https://ha-tpp12.sqlha.com:5008/vedsdk", + } + + err := validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextInvalidMode { + t.Fatalf("Expecting error %s but got %s", errorTextInvalidMode, err) + } + + entry = &roleEntry{ + TPPURL: "https://qa-tpp.exmple.com/vedsdk", + TPPUser: "admin", + TPPPassword: "xxxx", + TTL: 120, + MaxTTL: 100, + } + + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextValueMustBeLess { + t.Fatalf("Expecting error %s but got %s", errorTextValueMustBeLess, err) + } + + entry = &roleEntry{ + TPPURL: "https://qa-tpp.exmple.com/vedsdk", + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + TPPUser: "admin", + TPPPassword: "xxxx", + } + + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextTPPandCloudMixedCredentials { + t.Fatalf("Expecting error %s but got %s", errorTextTPPandCloudMixedCredentials, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreByCN: true, + StoreBy: "cn", + } + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextStoreByAndStoreByCNOrSerialConflict { + t.Fatalf("Expecting error %s but got %s", errorTextStoreByAndStoreByCNOrSerialConflict, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreBySerial: true, + StoreBy: "cn", + } + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextStoreByAndStoreByCNOrSerialConflict { + t.Fatalf("Expecting error %s but got %s", errorTextStoreByAndStoreByCNOrSerialConflict, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreBySerial: true, + StoreByCN: true, + StoreBy: "cn", + } + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextStoreByAndStoreByCNOrSerialConflict { + t.Fatalf("Expecting error %s but got %s", errorTextStoreByAndStoreByCNOrSerialConflict, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreBySerial: true, + StoreByCN: true, + NoStore: true, + } + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextNoStoreAndStoreByCNOrSerialConflict { + t.Fatalf("Expecting error %s but got %s", errorTextNoStoreAndStoreByCNOrSerialConflict, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreBy: "serial", + NoStore: true, + } + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + if err.Error() != errorTextNoStoreAndStoreByConflict { + t.Fatalf("Expecting error %s but got %s", errorTextNoStoreAndStoreByConflict, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreBy: "sebial", + } + err = validateEntry(entry) + if err == nil { + t.Fatalf("Expecting error") + } + expectingError := fmt.Sprintf(errTextStoreByWrongOption, storeBySerialString, storeByCNString, "sebial") + if err.Error() != expectingError { + t.Fatalf("Expecting error %s but got %s", expectingError, err) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreBySerial: true, + StoreByCN: true, + + } + err = validateEntry(entry) + if err != nil { + t.Fatal(err) + } + + if entry.StoreBy != storeBySerialString { + t.Fatalf("Expecting store_by parameter will be set to %s", storeBySerialString) + } + + entry = &roleEntry{ + Apikey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + StoreByCN: true, + + } + err = validateEntry(entry) + if err != nil { + t.Fatal(err) + } + + if entry.StoreBy != storeByCNString { + t.Fatalf("Expecting store_by parameter will be set to %s", storeBySerialString) + } +} diff --git a/plugin/pki/path_venafi_cert_enroll.go b/plugin/pki/path_venafi_cert_enroll.go index 02ec2634..385bcbfd 100644 --- a/plugin/pki/path_venafi_cert_enroll.go +++ b/plugin/pki/path_venafi_cert_enroll.go @@ -6,6 +6,7 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/helper/consts" "net" "strings" @@ -127,116 +128,46 @@ func (b *backend) pathVenafiCertObtain(ctx context.Context, req *logical.Request return logical.ErrorResponse(err.Error()), nil } - var commonName string var certReq *certificate.Request - if !signCSR { - commonName = data.Get("common_name").(string) - altNames := data.Get("alt_names").([]string) - ipSANs := data.Get("ip_sans").([]string) - if len(commonName) == 0 && len(altNames) == 0 { - return logical.ErrorResponse("no domains specified on certificate"), nil - } - if len(commonName) == 0 && len(altNames) > 0 { - commonName = altNames[0] - } - if !sliceContains(altNames, commonName) { - b.Logger().Debug(fmt.Sprintf("Adding CN %s to SAN %s because it wasn't included.", commonName, altNames)) - altNames = append(altNames, commonName) - } - certReq = &certificate.Request{ - Subject: pkix.Name{ - CommonName: commonName, - }, - CsrOrigin: certificate.LocalGeneratedCSR, - KeyPassword: data.Get("key_password").(string), - } - ipSet := make(map[string]struct{}) - nameSet := make(map[string]struct{}) - for _, v := range altNames { - if strings.Contains(v, "@") { - certReq.EmailAddresses = append(certReq.EmailAddresses, v) - } else if net.ParseIP(v) != nil { - ipSet[v] = struct{}{} - nameSet[v] = struct{}{} - } else { - nameSet[v] = struct{}{} - } - } - for _, v := range ipSANs { - if net.ParseIP(v) != nil { - ipSet[v] = struct{}{} - } - } - for ip := range ipSet { - certReq.IPAddresses = append(certReq.IPAddresses, net.ParseIP(ip)) - } - for k := range nameSet { - certReq.DNSNames = append(certReq.DNSNames, k) - } + var reqData requestData - } else { - b.Logger().Debug("Signing user provided CSR") - csrString := data.Get("csr").(string) - if csrString == "" { - return logical.ErrorResponse(fmt.Sprintf("\"csr\" is empty")), nil - } - pemBytes := []byte(csrString) - pemBlock, _ := pem.Decode(pemBytes) - if pemBlock == nil { - return logical.ErrorResponse(fmt.Sprintf("csr contains no data")), nil - } - csr, err := x509.ParseCertificateRequest(pemBlock.Bytes) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("can't parse provided CSR %s", err)), nil - } - commonName = csr.Subject.CommonName - certReq = &certificate.Request{ - CsrOrigin: certificate.UserProvidedCSR, - } - err = certReq.SetCSR(pemBytes) - if err != nil { - return nil, err - } + if data == nil { + return logical.ErrorResponse("data can't be nil"), nil } - if !signCSR { - if role.KeyType == "rsa" { - certReq.KeyLength = role.KeyBits - } else if role.KeyType == "ec" { - certReq.KeyType = certificate.KeyTypeECDSA - switch { - case role.KeyCurve == "P256": - certReq.KeyCurve = certificate.EllipticCurveP256 - case role.KeyCurve == "P384": - certReq.KeyCurve = certificate.EllipticCurveP384 - case role.KeyCurve == "P521": - certReq.KeyCurve = certificate.EllipticCurveP521 - default: - return logical.ErrorResponse(fmt.Sprintf("can't use key curve %s", role.KeyCurve)), nil - } + commonNameRaw, ok := data.GetOk("common_name") + if ok { + reqData.commonName = commonNameRaw.(string) + } - } else { - return logical.ErrorResponse(fmt.Sprintf("can't determine key algorithm for %s", role.KeyType)), nil - } + altNamesRaw, ok := data.GetOk("alt_names") + if ok { + reqData.altNames = altNamesRaw.([]string) } - if role.ChainOption == "first" { - certReq.ChainOption = certificate.ChainOptionRootFirst - } else if role.ChainOption == "last" { - certReq.ChainOption = certificate.ChainOptionRootLast - } else { - return logical.ErrorResponse(fmt.Sprintf("Invalid chain option %s", role.ChainOption)), nil + ipSANsRaw, ok := data.GetOk("ip_sans") + if ok { + reqData.ipSANs = ipSANsRaw.([]string) + } + + keyPasswordRaw, ok := data.GetOk("key_password") + if ok { + reqData.keyPassword = keyPasswordRaw.(string) + } + + csrStringRaw, ok := data.GetOk("csr") + if ok { + reqData.csrString = csrStringRaw.(string) } + err, certReq = formRequest(reqData, role, signCSR, b.Logger()) + b.Logger().Debug("Making certificate request") err = cl.GenerateRequest(nil, certReq) if err != nil { - return logical.ErrorResponse(err.Error()), nil + return logical.ErrorResponse(err.Error()), nil } - //Adding origin custom field with utility name to certificate metadata - certReq.CustomFields = []certificate.CustomField{{Type: certificate.CustomFieldOrigin, Value: utilityName}} - b.Logger().Debug("Running enroll request") requestID, err := cl.RequestCertificate(certReq) @@ -295,8 +226,8 @@ func (b *backend) pathVenafiCertObtain(ctx context.Context, req *logical.Request if !role.NoStore { if role.StoreBy == storeByCNString { //Writing certificate to the storage with CN - b.Logger().Debug("Putting certificate to the certs/" + commonName) - entry.Key = "certs/" + commonName + b.Logger().Debug("Putting certificate to the certs/" + reqData.commonName) + entry.Key = "certs/" + reqData.commonName if err := req.Storage.Put(ctx, entry); err != nil { b.Logger().Error("Error putting entry to storage: " + err.Error()) @@ -318,7 +249,7 @@ func (b *backend) pathVenafiCertObtain(ctx context.Context, req *logical.Request var respData map[string]interface{} if !signCSR { respData = map[string]interface{}{ - "common_name": commonName, + "common_name": reqData.commonName, "serial_number": serialNumber, "certificate_chain": chain, "certificate": pcc.Certificate, @@ -326,7 +257,7 @@ func (b *backend) pathVenafiCertObtain(ctx context.Context, req *logical.Request } } else { respData = map[string]interface{}{ - "common_name": commonName, + "common_name": reqData.commonName, "serial_number": serialNumber, "certificate_chain": chain, "certificate": pcc.Certificate, @@ -358,6 +289,119 @@ func (b *backend) pathVenafiCertObtain(ctx context.Context, req *logical.Request return logResp, nil } +type requestData struct { + commonName string + altNames []string + ipSANs []string + keyPassword string + csrString string +} + +func formRequest(reqData requestData, role *roleEntry, signCSR bool, logger hclog.Logger) (err error, certReq *certificate.Request){ + + + if !signCSR { + if len(reqData.commonName) == 0 && len(reqData.altNames) == 0 { + return fmt.Errorf("no domains specified on certificate"), certReq + } + if len(reqData.commonName) == 0 && len(reqData.altNames) > 0 { + reqData.commonName = reqData.altNames[0] + } + if !sliceContains(reqData.altNames, reqData.commonName) { + logger.Debug(fmt.Sprintf("Adding CN %s to SAN %s because it wasn't included.", reqData.commonName, reqData.altNames)) + reqData.altNames = append(reqData.altNames, reqData.commonName) + } + certReq = &certificate.Request{ + Subject: pkix.Name{ + CommonName: reqData.commonName, + }, + CsrOrigin: certificate.LocalGeneratedCSR, + KeyPassword: reqData.keyPassword, + } + ipSet := make(map[string]struct{}) + nameSet := make(map[string]struct{}) + for _, v := range reqData.altNames { + if strings.Contains(v, "@") { + certReq.EmailAddresses = append(certReq.EmailAddresses, v) + } else if net.ParseIP(v) != nil { + ipSet[v] = struct{}{} + nameSet[v] = struct{}{} + } else { + nameSet[v] = struct{}{} + } + } + for _, v := range reqData.ipSANs { + if net.ParseIP(v) != nil { + ipSet[v] = struct{}{} + } + } + for ip := range ipSet { + certReq.IPAddresses = append(certReq.IPAddresses, net.ParseIP(ip)) + } + for k := range nameSet { + certReq.DNSNames = append(certReq.DNSNames, k) + } + + } else { + logger.Debug("Signing user provided CSR") + + if reqData.csrString == "" { + return fmt.Errorf(fmt.Sprintf("\"csr\" is empty")), certReq + } + pemBytes := []byte(reqData.csrString) + pemBlock, _ := pem.Decode(pemBytes) + if pemBlock == nil { + return fmt.Errorf(fmt.Sprintf("csr contains no data")), certReq + } + csr, err := x509.ParseCertificateRequest(pemBlock.Bytes) + if err != nil { + return fmt.Errorf(fmt.Sprintf("can't parse provided CSR %s", err)), certReq + } + reqData.commonName = csr.Subject.CommonName + certReq = &certificate.Request{ + CsrOrigin: certificate.UserProvidedCSR, + } + err = certReq.SetCSR(pemBytes) + if err != nil { + return err, certReq + } + } + + if !signCSR { + if role.KeyType == "rsa" { + certReq.KeyLength = role.KeyBits + } else if role.KeyType == "ec" { + certReq.KeyType = certificate.KeyTypeECDSA + switch { + case role.KeyCurve == "P256": + certReq.KeyCurve = certificate.EllipticCurveP256 + case role.KeyCurve == "P384": + certReq.KeyCurve = certificate.EllipticCurveP384 + case role.KeyCurve == "P521": + certReq.KeyCurve = certificate.EllipticCurveP521 + default: + return fmt.Errorf(fmt.Sprintf("can't use key curve %s", role.KeyCurve)), certReq + } + + } else { + return fmt.Errorf(fmt.Sprintf("can't determine key algorithm for %s", role.KeyType)), certReq + } + } + + if role.ChainOption == "first" { + certReq.ChainOption = certificate.ChainOptionRootFirst + } else if role.ChainOption == "last" { + certReq.ChainOption = certificate.ChainOptionRootLast + } else { + return fmt.Errorf(fmt.Sprintf("Invalid chain option %s", role.ChainOption)), certReq + } + + //Adding origin custom field with utility name to certificate metadata + certReq.CustomFields = []certificate.CustomField{{Type: certificate.CustomFieldOrigin, Value: utilityName}} + + return nil, certReq +} + type VenafiCert struct { Certificate string `json:"certificate"` CertificateChain string `json:"certificate_chain"` diff --git a/plugin/pki/path_venafi_cert_enroll_test.go b/plugin/pki/path_venafi_cert_enroll_test.go new file mode 100644 index 00000000..55cfa323 --- /dev/null +++ b/plugin/pki/path_venafi_cert_enroll_test.go @@ -0,0 +1,25 @@ +package pki + +import ( + "testing" +) + +func TestOriginInRequest(t *testing.T) { + integrationTestEnv, err := newIntegrationTestEnv() + if err != nil { + t.Fatal(err) + } + + signCSR := false + var data requestData + var role roleEntry + + data.commonName = "tpp.example.com" + role.KeyType = "rsa" + role.ChainOption = "first" + + err, certReq := formRequest(data, &role, signCSR, integrationTestEnv.Backend.Logger()) + if certReq.CustomFields[0].Value != utilityName { + t.Fatalf("Expected %s in request custom fields origin", utilityName) + } +} diff --git a/plugin/pki/path_venafi_cert_read.go b/plugin/pki/path_venafi_cert_read.go index da4a7272..5d14201e 100644 --- a/plugin/pki/path_venafi_cert_read.go +++ b/plugin/pki/path_venafi_cert_read.go @@ -18,7 +18,7 @@ func pathVenafiCertRead(b *backend) *framework.Path { }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathVenafiCertRead, - //todo: maybe add delete operation to delete certificate entry from storage + //todo: maybe add delete operation to delete certificate entry from storage }, HelpSynopsis: pathConfigRootHelpSyn,