aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVasily Gerasimov <UgnineSirdis@ydb.tech>2025-02-28 15:01:14 +0200
committerGitHub <noreply@github.com>2025-02-28 15:01:14 +0200
commitc5b531e5ab62ce07f4aaf50552679425d810459c (patch)
tree5625fdb100eccc95057510a23f8125fe60734245
parent1547cff981d4a39f2a007760ca202b1071a95cd3 (diff)
downloadydb-c5b531e5ab62ce07f4aaf50552679425d810459c.tar.gz
Client certificates options in ydb cli (#8406)
-rw-r--r--ydb/apps/dstool/lib/common.py3
-rw-r--r--ydb/apps/ydb/CHANGELOG.md1
-rw-r--r--ydb/core/driver_lib/cli_base/cli_cmds_db.cpp2
-rw-r--r--ydb/core/driver_lib/cli_base/cli_cmds_root.cpp5
-rw-r--r--ydb/core/driver_lib/cli_base/cli_grpc.h3
-rw-r--r--ydb/core/driver_lib/cli_utils/cli_cmds_root.cpp5
-rw-r--r--ydb/core/driver_lib/cli_utils/cli_cmds_tenant.cpp2
-rw-r--r--ydb/core/driver_lib/run/main.cpp6
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_command.cpp1
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_profile.cpp91
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_profile.h5
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_root_common.cpp96
-rw-r--r--ydb/public/lib/ydb_cli/commands/ydb_root_common.h11
-rw-r--r--ydb/public/lib/ydb_cli/common/cert_format_converter.cpp207
-rw-r--r--ydb/public/lib/ydb_cli/common/cert_format_converter.h36
-rw-r--r--ydb/public/lib/ydb_cli/common/cert_format_converter_ut.cpp105
-rw-r--r--ydb/public/lib/ydb_cli/common/command.h12
-rw-r--r--ydb/public/lib/ydb_cli/common/root.cpp34
-rw-r--r--ydb/public/lib/ydb_cli/common/root.h4
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ca-private-key.pem6
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/cert-ext.cfg10
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/cert.cfg15
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-all-pwd.pem27
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-all.pem24
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert-pwd.p12bin0 -> 1509 bytes
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.p12bin0 -> 1509 bytes
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.pem18
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pkcs8.pem0
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pwd.pem9
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key.pem6
-rwxr-xr-xydb/public/lib/ydb_cli/common/ut/test_data/generate_certs.sh49
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/root-ca.pem14
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all-pwd.pem51
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all.pem49
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert-pwd.p12bin0 -> 2726 bytes
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.p12bin0 -> 2726 bytes
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.pem21
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key-pwd.pem30
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key.pem28
-rw-r--r--ydb/public/lib/ydb_cli/common/ut/ya.make1
-rw-r--r--ydb/public/lib/ydb_cli/common/ya.make2
-rw-r--r--ydb/public/sdk/cpp/client/ydb_driver/driver.h2
-rw-r--r--ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h2
43 files changed, 962 insertions, 31 deletions
diff --git a/ydb/apps/dstool/lib/common.py b/ydb/apps/dstool/lib/common.py
index 9f7b539126..fae1480f15 100644
--- a/ydb/apps/dstool/lib/common.py
+++ b/ydb/apps/dstool/lib/common.py
@@ -219,7 +219,8 @@ class ConnectionParams:
g.add_argument('--grpc-port', type=int, default=2135, metavar='PORT', help='GRPC port to use for procedure invocation')
g.add_argument('--mon-port', type=int, default=8765, metavar='PORT', help='HTTP monitoring port for viewer JSON access')
g.add_argument('--token-file', type=FileType(encoding='ascii'), metavar='PATH', help='Path to token file')
- g.add_argument('--ca-file', metavar='PATH', dest='cafile', type=str, help='Path to a file containing the PEM encoding of the server root certificates for tls connections.')
+ g.add_argument('--ca-file', metavar='PATH', dest='cafile', type=str, help='File containing PEM encoded root certificates for SSL/TLS connections. '
+ 'If this parameter is empty, the default roots will be used.')
g.add_argument('--http-timeout', type=int, default=5, help='Timeout for blocking socket I/O operations during HTTP(s) queries')
g.add_argument('--insecure', action='store_true', help='Allow insecure HTTPS fetching')
g.add_argument('--use-ip', action='store_true', help='Use IP addresses instead of hostnames when connecting to endpoints')
diff --git a/ydb/apps/ydb/CHANGELOG.md b/ydb/apps/ydb/CHANGELOG.md
index d26ff9d0f1..54ad8d414e 100644
--- a/ydb/apps/ydb/CHANGELOG.md
+++ b/ydb/apps/ydb/CHANGELOG.md
@@ -1,3 +1,4 @@
+* Add options for client certificates in SSL/TLS connections.
* Add `ydb admin node config init` command to initialize directory with node config files.
* Add `ydb admin cluster config generate` command to generate dynamic config from static config on cluster.
* Fixed memory leak in tpcds generator.
diff --git a/ydb/core/driver_lib/cli_base/cli_cmds_db.cpp b/ydb/core/driver_lib/cli_base/cli_cmds_db.cpp
index a1e823a94b..5098375100 100644
--- a/ydb/core/driver_lib/cli_base/cli_cmds_db.cpp
+++ b/ydb/core/driver_lib/cli_base/cli_cmds_db.cpp
@@ -883,6 +883,8 @@ public:
ClientConfig.MaxInFlight = CommandConfig.ClientConfig.MaxInFlight;
ClientConfig.EnableSsl = CommandConfig.ClientConfig.EnableSsl;
ClientConfig.SslCredentials.pem_root_certs = CommandConfig.ClientConfig.SslCredentials.pem_root_certs;
+ ClientConfig.SslCredentials.pem_cert_chain = CommandConfig.ClientConfig.SslCredentials.pem_cert_chain;
+ ClientConfig.SslCredentials.pem_private_key = CommandConfig.ClientConfig.SslCredentials.pem_private_key;
}
template<typename T>
diff --git a/ydb/core/driver_lib/cli_base/cli_cmds_root.cpp b/ydb/core/driver_lib/cli_base/cli_cmds_root.cpp
index a42c7cb20a..d86ecb0e01 100644
--- a/ydb/core/driver_lib/cli_base/cli_cmds_root.cpp
+++ b/ydb/core/driver_lib/cli_base/cli_cmds_root.cpp
@@ -186,6 +186,7 @@ public:
throw TMisuseException() << message;
}
ParseCaCerts(config);
+ ParseClientCert(config);
config.Address = Address;
if (!hostname) {
@@ -195,6 +196,10 @@ public:
if (config.EnableSsl) {
CommandConfig.ClientConfig.EnableSsl = config.EnableSsl;
CommandConfig.ClientConfig.SslCredentials.pem_root_certs = config.CaCerts;
+ if (config.ClientCert) {
+ CommandConfig.ClientConfig.SslCredentials.pem_cert_chain = config.ClientCert;
+ CommandConfig.ClientConfig.SslCredentials.pem_private_key = config.ClientCertPrivateKey;
+ }
}
}
diff --git a/ydb/core/driver_lib/cli_base/cli_grpc.h b/ydb/core/driver_lib/cli_base/cli_grpc.h
index 21b5542311..ca02f4ab6c 100644
--- a/ydb/core/driver_lib/cli_base/cli_grpc.h
+++ b/ydb/core/driver_lib/cli_base/cli_grpc.h
@@ -94,6 +94,8 @@ public:
ClientConfig.MaxInFlight = CommandConfig.ClientConfig.MaxInFlight;
ClientConfig.EnableSsl = CommandConfig.ClientConfig.EnableSsl;
ClientConfig.SslCredentials.pem_root_certs = CommandConfig.ClientConfig.SslCredentials.pem_root_certs;
+ ClientConfig.SslCredentials.pem_cert_chain = CommandConfig.ClientConfig.SslCredentials.pem_cert_chain;
+ ClientConfig.SslCredentials.pem_private_key = CommandConfig.ClientConfig.SslCredentials.pem_private_key;
}
static int PrepareConfigCredentials(NGRpcProxy::TGRpcClientConfig clientConfig, TConfig& commandConfig) {
@@ -159,4 +161,3 @@ public:
}
}
-
diff --git a/ydb/core/driver_lib/cli_utils/cli_cmds_root.cpp b/ydb/core/driver_lib/cli_utils/cli_cmds_root.cpp
index e06d12e51b..218904fb30 100644
--- a/ydb/core/driver_lib/cli_utils/cli_cmds_root.cpp
+++ b/ydb/core/driver_lib/cli_utils/cli_cmds_root.cpp
@@ -56,11 +56,16 @@ public:
config.EnableSsl = endpoint.EnableSsl.GetRef();
}
ParseCaCerts(config);
+ ParseClientCert(config);
CommandConfig.ClientConfig = NYdbGrpc::TGRpcClientConfig(endpoint.Address);
if (config.EnableSsl) {
CommandConfig.ClientConfig.EnableSsl = config.EnableSsl;
CommandConfig.ClientConfig.SslCredentials.pem_root_certs = config.CaCerts;
+ if (config.ClientCert) {
+ CommandConfig.ClientConfig.SslCredentials.pem_cert_chain = config.ClientCert;
+ CommandConfig.ClientConfig.SslCredentials.pem_private_key = config.ClientCertPrivateKey;
+ }
}
}
};
diff --git a/ydb/core/driver_lib/cli_utils/cli_cmds_tenant.cpp b/ydb/core/driver_lib/cli_utils/cli_cmds_tenant.cpp
index e8f527886c..0c783b079e 100644
--- a/ydb/core/driver_lib/cli_utils/cli_cmds_tenant.cpp
+++ b/ydb/core/driver_lib/cli_utils/cli_cmds_tenant.cpp
@@ -142,6 +142,8 @@ public:
ClientConfig.MaxInFlight = CommandConfig.ClientConfig.MaxInFlight;
ClientConfig.EnableSsl = CommandConfig.ClientConfig.EnableSsl;
ClientConfig.SslCredentials.pem_root_certs = CommandConfig.ClientConfig.SslCredentials.pem_root_certs;
+ ClientConfig.SslCredentials.pem_cert_chain = CommandConfig.ClientConfig.SslCredentials.pem_cert_chain;
+ ClientConfig.SslCredentials.pem_private_key = CommandConfig.ClientConfig.SslCredentials.pem_private_key;
}
int Run(TConfig &config) override
diff --git a/ydb/core/driver_lib/run/main.cpp b/ydb/core/driver_lib/run/main.cpp
index b26225fc14..00be7c4568 100644
--- a/ydb/core/driver_lib/run/main.cpp
+++ b/ydb/core/driver_lib/run/main.cpp
@@ -89,8 +89,11 @@ int MainRun(const TKikimrRunConfig& runConfig, std::shared_ptr<TModuleFactories>
configParser.SetupGlobalOpts(opts);
NMsgBusProxy::TMsgBusClientConfig mbusConfig;
mbusConfig.ConfigureLastGetopt(opts, "mb-");
- opts.AddLongOption("ca-file", "Path to a file containing the PEM encoding of the server root certificates for tls connections.\n").RequiredArgument("PATH");
+ opts.AddLongOption("ca-file", "File containing PEM encoded root certificates for SSL/TLS connections. If this parameter is empty, the default roots will be used.\n").RequiredArgument("PATH");
NDriverClient::HideOptions(opts);
+ opts.AddLongOption("client-cert-file", "File containing client certificate for SSL/TLS connections (PKCS#12 or PEM-encoded)").RequiredArgument("PATH");
+ opts.AddLongOption("client-cert-key-file", "File containing PEM encoded client certificate private key for SSL/TLS connections").RequiredArgument("PATH");
+ opts.AddLongOption("client-cert-key-password-file", "File containing password for client certificate private key (if key is encrypted). If key file is encrypted, but this option is not set, password will be asked interactively").RequiredArgument("PATH");
opts.AddLongOption('s', "server", "Server address to connect (default $KIKIMR_SERVER)").RequiredArgument("ADDR[:NUM]");
opts.AddLongOption('k', "token", "Security token").RequiredArgument("TOKEN");
opts.AddLongOption('f', "token-file", "Security token file").RequiredArgument("PATH");
@@ -203,4 +206,3 @@ int ParameterizedMain(int argc, char **argv, std::shared_ptr<NKikimr::TModuleFac
return 1;
}
}
-
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_command.cpp b/ydb/public/lib/ydb_cli/commands/ydb_command.cpp
index cfdbae4fa8..24c19a0bc7 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_command.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_command.cpp
@@ -29,6 +29,7 @@ TDriverConfig TYdbCommand::CreateDriverConfig(TConfig& config) {
driverConfig.UseSecureConnection(config.CaCerts);
if (config.IsNetworkIntensive)
driverConfig.SetNetworkThreadsNum(16);
+ driverConfig.UseClientCertificate(config.ClientCert, config.ClientCertPrivateKey);
return driverConfig;
}
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_profile.cpp b/ydb/public/lib/ydb_cli/commands/ydb_profile.cpp
index 903ba21628..7afbc969df 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_profile.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_profile.cpp
@@ -244,6 +244,15 @@ namespace {
if (profile->Has("ca-file")) {
Cout << " ca-file: " << profile->GetValue("ca-file").as<TString>() << Endl;
}
+ if (profile->Has("client-cert-file")) {
+ Cout << " client-cert-file: " << profile->GetValue("client-cert-file").as<TString>() << Endl;
+ }
+ if (profile->Has("client-cert-key-file")) {
+ Cout << " client-cert-key-file: " << profile->GetValue("client-cert-key-file").as<TString>() << Endl;
+ }
+ if (profile->Has("client-cert-key-password-file")) {
+ Cout << " client-cert-key-password-file: " << profile->GetValue("client-cert-key-password-file").as<TString>() << Endl;
+ }
}
}
@@ -307,6 +316,15 @@ void TCommandConnectionInfo::PrintInfo(TConfig& config) {
if (config.CaCertsFile) {
Cout << "ca-file: " << config.CaCertsFile << Endl;
}
+ if (config.ClientCertFile) {
+ Cout << "client-cert-file: " << config.ClientCertFile << Endl;
+ }
+ if (config.ClientCertPrivateKeyFile) {
+ Cout << "client-cert-key-file: " << config.ClientCertPrivateKeyFile << Endl;
+ }
+ if (config.ClientCertPrivateKeyPasswordFile) {
+ Cout << "client-cert-key-password-file: " << config.ClientCertPrivateKeyPasswordFile << Endl;
+ }
}
void TCommandConnectionInfo::PrintVerboseInfo(TConfig& config) {
@@ -382,7 +400,10 @@ void TCommandProfileCommon::GetOptionsFromStdin() {
{"user", User},
{"password-file", PasswordFile},
{"iam-endpoint", IamEndpoint},
- {"ca-file", CaCertsFile}
+ {"ca-file", CaCertsFile},
+ {"client-cert-file", ClientCertFile},
+ {"client-cert-key-file", ClientCertPrivateKeyFile},
+ {"client-cert-key-password-file", ClientCertPrivateKeyPasswordFile},
};
while (Cin.ReadLine(line)) {
Strip(line, trimmedLine);
@@ -432,6 +453,15 @@ void TCommandProfileCommon::ConfigureProfile(const TString& profileName, std::sh
if (cmdLine && CaCertsFile) {
profile->SetValue("ca-file", CaCertsFile);
}
+ if (cmdLine && ClientCertFile) {
+ profile->SetValue("client-cert-file", ClientCertFile);
+ }
+ if (cmdLine && ClientCertPrivateKeyFile) {
+ profile->SetValue("client-cert-key-file", ClientCertPrivateKeyFile);
+ }
+ if (cmdLine && ClientCertPrivateKeyPasswordFile) {
+ profile->SetValue("client-cert-key-password-file", ClientCertPrivateKeyPasswordFile);
+ }
if (interactive) {
TString activeProfileName = profileManager->GetActiveProfileName();
@@ -673,7 +703,8 @@ bool TCommandProfileCommon::AnyProfileOptionInCommandLine() {
return Endpoint || Database || TokenFile || Oauth2KeyFile ||
IamTokenFile || YcTokenFile ||
SaKeyFile || UseMetadataCredentials || User ||
- PasswordFile || IamEndpoint || AnonymousAuth || CaCertsFile;
+ PasswordFile || IamEndpoint || AnonymousAuth || CaCertsFile ||
+ ClientCertFile || ClientCertPrivateKeyFile || ClientCertPrivateKeyPasswordFile;
}
TCommandCreateProfile::TCommandCreateProfile()
@@ -711,8 +742,17 @@ void TCommandProfileCommon::Config(TConfig& config) {
.RequiredArgument("STR").StoreResult(&IamEndpoint);
}
opts.AddLongOption("ca-file",
- "Path to a file containing the PEM encoding of the server root certificates for tls connections.")
+ "File containing PEM encoded root certificates for SSL/TLS connections.")
.RequiredArgument("PATH").StoreResult(&CaCertsFile);
+ opts.AddLongOption("client-cert-file",
+ "File containing client certificate for SSL/TLS connections (PKCS#12 or PEM-encoded).")
+ .RequiredArgument("PATH").StoreResult(&ClientCertFile);
+ opts.AddLongOption("client-cert-key-file",
+ "File containing PEM encoded client certificate private key for SSL/TLS connections.")
+ .RequiredArgument("PATH").StoreResult(&ClientCertPrivateKeyFile);
+ opts.AddLongOption("client-cert-key-password-file",
+ "File containing password for client certificate private key (if key is encrypted). If key file is encrypted, but this option is not set, password will be asked interactively.")
+ .RequiredArgument("PATH").StoreResult(&ClientCertPrivateKeyPasswordFile);
if (!IsStdinInteractive()) {
GetOptionsFromStdin();
}
@@ -1064,8 +1104,14 @@ void TCommandUpdateProfile::Config(TConfig& config) {
if (config.UseIamAuth) {
opts.AddLongOption("no-iam-endpoint", "Delete endpoint of IAM service from the profile").StoreTrue(&NoIamEndpoint);
}
- opts.AddLongOption("no-ca-file", "Delete path to file containing the PEM encoding of the "
- "server root certificates for tls connections from the profile").StoreTrue(&NoCaCertsFile);
+ opts.AddLongOption("no-ca-file", "Delete path to file containing the PEM encoded "
+ "root certificates for SSL/TLS connections from the profile").StoreTrue(&NoCaCertsFile);
+ opts.AddLongOption("no-client-cert-file", "Delete path to file containing client certificate "
+ "for SSL/TLS connections").StoreTrue(&NoClientCertFile);
+ opts.AddLongOption("no-client-cert-key-file", "Delete path to file containing PEM encoded client "
+ "certificate private key for SSL/TLS connections").StoreTrue(&NoClientCertPrivateKeyFile);
+ opts.AddLongOption("no-client-cert-key-password-file", "Delete path to file containing password for "
+ "client certificate private key (if key is encrypted)").StoreTrue(&NoClientCertPrivateKeyPasswordFile);
}
void TCommandUpdateProfile::ValidateNoOptions() {
@@ -1080,21 +1126,21 @@ void TCommandUpdateProfile::ValidateNoOptions() {
throw TMisuseException() << "You cannot enter authentication options and the \"--no-auth\" option at the same time";
}
TStringBuilder str;
- if (Endpoint && NoEndpoint) {
- str << "\"--endpoint\" and \"--no-endpoint\"";
- } else {
- if (Database && NoDatabase) {
- str << "\"--database and \"--no-database\"";
- } else {
- if (IamEndpoint && NoIamEndpoint) {
- str << "\"--iam-endpoint\" and \"--no-iam-endpoint\"";
- } else {
- if (CaCertsFile && NoCaCertsFile) {
- str << "\"--ca-file\" and \"--no-ca-file\"";
- }
+ auto addMutuallyExclusiveOptionError = [&](bool validationResult, TStringBuf optionName) {
+ if (validationResult) {
+ if (str) {
+ str << ", ";
}
+ str << "\"--" << optionName << "\" and \"--no-" << optionName << "\"";
}
- }
+ };
+ addMutuallyExclusiveOptionError(Endpoint && NoEndpoint, "endpoint");
+ addMutuallyExclusiveOptionError(Database && NoDatabase, "database");
+ addMutuallyExclusiveOptionError(IamEndpoint && NoIamEndpoint, "iam-endpoint");
+ addMutuallyExclusiveOptionError(CaCertsFile && NoCaCertsFile, "ca-file");
+ addMutuallyExclusiveOptionError(ClientCertFile && NoClientCertFile, "client-cert-file");
+ addMutuallyExclusiveOptionError(ClientCertPrivateKeyFile && NoClientCertPrivateKeyFile, "client-cert-key-file");
+ addMutuallyExclusiveOptionError(NoClientCertPrivateKeyPasswordFile && NoClientCertPrivateKeyPasswordFile, "client-cert-key-password-file");
if (!str.empty()) {
throw TMisuseException() << "Options " << str << " are mutually exclusive";
}
@@ -1116,6 +1162,15 @@ void TCommandUpdateProfile::DropNoOptions(std::shared_ptr<IProfile> profile) {
if (NoCaCertsFile) {
profile->RemoveValue("ca-file");
}
+ if (NoClientCertFile) {
+ profile->RemoveValue("client-cert-file");
+ }
+ if (NoClientCertPrivateKeyFile) {
+ profile->RemoveValue("client-cert-key-file");
+ }
+ if (NoClientCertPrivateKeyPasswordFile) {
+ profile->RemoveValue("client-cert-key-password-file");
+ }
}
void TCommandUpdateProfile::Parse(TConfig& config) {
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_profile.h b/ydb/public/lib/ydb_cli/commands/ydb_profile.h
index d4df0f3669..34a1ff00da 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_profile.h
+++ b/ydb/public/lib/ydb_cli/commands/ydb_profile.h
@@ -43,7 +43,7 @@ protected:
TConfig& config, bool interactive, bool cmdLine);
TString ProfileName, Endpoint, Database, TokenFile, Oauth2KeyFile, YcTokenFile, SaKeyFile,
- IamTokenFile, IamEndpoint, User, PasswordFile, CaCertsFile;
+ IamTokenFile, IamEndpoint, User, PasswordFile, CaCertsFile, ClientCertFile, ClientCertPrivateKeyFile, ClientCertPrivateKeyPasswordFile;
bool UseMetadataCredentials = false;
bool AnonymousAuth = false;
@@ -141,6 +141,9 @@ private:
bool NoAuth = false;
bool NoIamEndpoint = false;
bool NoCaCertsFile = false;
+ bool NoClientCertFile = false;
+ bool NoClientCertPrivateKeyFile = false;
+ bool NoClientCertPrivateKeyPasswordFile = false;
};
class TCommandReplaceProfile : public TCommandProfileCommon {
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_root_common.cpp b/ydb/public/lib/ydb_cli/commands/ydb_root_common.cpp
index 7e176b5424..bd2af7886d 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_root_common.cpp
+++ b/ydb/public/lib/ydb_cli/commands/ydb_root_common.cpp
@@ -18,6 +18,7 @@
#include "ydb_workload.h"
#include <ydb/public/lib/ydb_cli/commands/interactive/interactive_cli.h>
+#include <ydb/public/lib/ydb_cli/common/cert_format_converter.h>
#include <ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/credentials.h>
#include <ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/from_file.h>
#include <ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/jwt_token_source.h>
@@ -352,18 +353,19 @@ void TClientCommandRootCommon::ExtractParams(TConfig& config) {
ParseDatabase(config);
ParseAddress(config);
ParseCaCerts(config);
+ ParseClientCert(config);
ParseIamEndpoint(config);
ParseCredentials(config);
}
namespace {
- inline void PrintSettingFromProfile(const TString& setting, std::shared_ptr<IProfile> profile, bool explicitOption) {
+ inline void PrintSettingFromProfile(const TString& setting, const std::shared_ptr<IProfile>& profile, bool explicitOption) {
Cerr << "Using " << setting << " due to configuration in" << (explicitOption ? "" : " active") << " profile \""
<< profile->GetName() << "\"" << (explicitOption ? " from explicit --profile option" : "") << Endl;
}
- inline TString GetProfileSource(std::shared_ptr<IProfile> profile, bool explicitOption) {
+ inline TString GetProfileSource(const std::shared_ptr<IProfile>& profile, bool explicitOption) {
Y_ABORT_UNLESS(profile, "No profile to get source");
if (explicitOption) {
return TStringBuilder() << "profile \"" << profile->GetName() << "\" from explicit --profile option";
@@ -372,7 +374,7 @@ namespace {
}
}
-bool TClientCommandRootCommon::TryGetParamFromProfile(const TString& name, std::shared_ptr<IProfile> profile, bool explicitOption,
+bool TClientCommandRootCommon::TryGetParamFromProfile(const TString& name, const std::shared_ptr<IProfile>& profile, bool explicitOption,
std::function<bool(const TString&, const TString&, bool)> callback) {
if (profile && profile->Has(name)) {
return callback(profile->GetValue(name).as<TString>(), GetProfileSource(profile, explicitOption), explicitOption);
@@ -380,6 +382,34 @@ bool TClientCommandRootCommon::TryGetParamFromProfile(const TString& name, std::
return false;
}
+bool TClientCommandRootCommon::TryGetParamsPackFromProfile(const std::shared_ptr<IProfile>& profile, bool explicitOption,
+ std::function<bool(const TString& /*source*/, bool /*explicit*/, const std::vector<TString>& /*values*/)> callback,
+ const std::initializer_list<TString>& names) {
+ if (!profile) {
+ return false;
+ }
+ bool hasAtLeastOne = false;
+ for (const TString& name : names) {
+ if (profile->Has(name)) {
+ hasAtLeastOne = true;
+ break;
+ }
+ }
+ if (hasAtLeastOne) {
+ std::vector<TString> values;
+ values.reserve(names.size());
+ for (const TString& name : names) {
+ if (profile->Has(name)) {
+ values.emplace_back(profile->GetValue(name).as<TString>());
+ } else {
+ values.emplace_back();
+ }
+ }
+ return callback(GetProfileSource(profile, explicitOption), explicitOption, values);
+ }
+ return false;
+}
+
void TClientCommandRootCommon::ParseCaCerts(TConfig& config) {
auto getCaFile = [this, &config] (const TString& param, const TString& sourceText, bool explicitOption) {
if (!IsCaCertsFileSet && (explicitOption || !Profile)) {
@@ -407,16 +437,74 @@ void TClientCommandRootCommon::ParseCaCerts(TConfig& config) {
}
}
+void TClientCommandRootCommon::ParseClientCert(TConfig& config) {
+ auto getClientCertFiles = [this, &config] (const TString& sourceText, bool explicitOption, const std::vector<TString>& values) {
+ Y_ABORT_UNLESS(values.size() == 3);
+ const TString& clientCertFileParam = values[0];
+ const TString& clientCertPrivateKeyFileParam = values[1];
+ const TString& clientCertPrivateKeyPasswordFileParam = values[2];
+ if (!IsClientCertFileSet && (explicitOption || !Profile)) {
+ config.ClientCertFile = clientCertFileParam;
+ config.ClientCertPrivateKeyFile = clientCertPrivateKeyFileParam;
+ config.ClientCertPrivateKeyPasswordFile = clientCertPrivateKeyPasswordFileParam;
+ IsClientCertFileSet = true;
+ GetClientCert(config);
+ }
+ if (!IsVerbose()) {
+ return true;
+ }
+ Cerr << "Using client certificate from file: " << clientCertFileParam << Endl;
+ config.ConnectionParams["client-cert-file"].push_back({clientCertFileParam, sourceText});
+ config.ConnectionParams["client-cert-key-file"].push_back({clientCertPrivateKeyFileParam, sourceText});
+ config.ConnectionParams["client-cert-key-password-file"].push_back({clientCertPrivateKeyPasswordFileParam, sourceText});
+ return false;
+ };
+ // Priority 1. Explicit --client-cert-file/--client-cert-key-file options
+ if (ClientCertFile) {
+ if (getClientCertFiles("explicit --client-cert-file/--client-cert-key-file/--client-cert-key-password-file options", true, { ClientCertFile, ClientCertPrivateKeyFile, ClientCertPrivateKeyPasswordFile })) {
+ return;
+ }
+ }
+ // Priority 2. Explicit --profile option
+ if (TryGetParamsPackFromProfile(Profile, true, getClientCertFiles, { "client-cert-file", "client-cert-key-file", "client-cert-key-password-file" })) {
+ return;
+ }
+ // Priority 3. Active profile (if --profile option is not specified)
+ if (TryGetParamsPackFromProfile(ProfileManager->GetActiveProfile(), false, getClientCertFiles, { "client-cert-file", "client-cert-key-file", "client-cert-key-password-file" })) {
+ return;
+ }
+}
+
void TClientCommandRootCommon::GetCaCerts(TConfig& config) {
if (!config.EnableSsl && !config.CaCertsFile.empty()) {
throw TMisuseException()
- << "\"ca-file\" option provided for a non-ssl connection. Use grpcs:// prefix for host to connect using SSL.";
+ << "\"ca-file\" option is provided for a non-ssl connection. Use grpcs:// prefix for host to connect using SSL.";
}
if (!config.CaCertsFile.empty()) {
config.CaCerts = ReadFromFile(config.CaCertsFile, "CA certificates");
}
}
+void TClientCommandRootCommon::GetClientCert(TConfig& config) {
+ if (!config.EnableSsl && !config.ClientCertFile.empty()) {
+ throw TMisuseException()
+ << "\"client-cert-file\"/\"client-cert-key-file\"/\"client-cert-key-password-file\" options are provided for a non-ssl connection. Use grpcs:// prefix for host to connect using SSL.";
+ }
+ if (!config.ClientCertFile.empty()) {
+ config.ClientCert = ReadFromFile(config.ClientCertFile, "Client certificate");
+ }
+ if (!config.ClientCertPrivateKeyFile.empty()) {
+ config.ClientCertPrivateKey = ReadFromFile(config.ClientCertPrivateKeyFile, "Client certificate private key");
+ }
+ if (!config.ClientCertPrivateKeyPasswordFile.empty()) {
+ config.ClientCertPrivateKeyPassword = ReadFromFile(config.ClientCertPrivateKeyPasswordFile, "Client certificate private key password");
+ }
+
+ // Convert certificates from PKCS#12 to PEM or encrypted private key to nonencrypted
+ // May ask for password
+ std::tie(config.ClientCert, config.ClientCertPrivateKey) = ConvertCertToPEM(config.ClientCert, config.ClientCertPrivateKey, config.ClientCertPrivateKeyPassword);
+}
+
void TClientCommandRootCommon::ParseAddress(TConfig& config) {
auto getAddress = [this, &config] (const TString& param, const TString& sourceText, bool explicitOption) {
TString address;
diff --git a/ydb/public/lib/ydb_cli/commands/ydb_root_common.h b/ydb/public/lib/ydb_cli/commands/ydb_root_common.h
index 2e7d353fb1..d55580191d 100644
--- a/ydb/public/lib/ydb_cli/commands/ydb_root_common.h
+++ b/ydb/public/lib/ydb_cli/commands/ydb_root_common.h
@@ -52,12 +52,20 @@ private:
void ParseDatabase(TConfig& config);
void ParseIamEndpoint(TConfig& config);
void ParseCaCerts(TConfig& config) override;
+ void ParseClientCert(TConfig& config) override;
void GetAddressFromString(TConfig& config, TString* result = nullptr);
bool ParseProtocolNoConfig(TString& message);
void GetCaCerts(TConfig& config);
- bool TryGetParamFromProfile(const TString& name, std::shared_ptr<IProfile> profile, bool explicitOption,
+ void GetClientCert(TConfig& config);
+ bool TryGetParamFromProfile(const TString& name, const std::shared_ptr<IProfile>& profile, bool explicitOption,
std::function<bool(const TString&, const TString&, bool)> callback);
+ // Gets more than one params from one profile source.
+ // Returns true if at least one of the params are found in profile.
+ bool TryGetParamsPackFromProfile(const std::shared_ptr<IProfile>& profile, bool explicitOption,
+ std::function<bool(const TString& /*source*/, bool /*explicit*/, const std::vector<TString>& /*values*/)> callback,
+ const std::initializer_list<TString>& names);
+
TString Database;
ui32 VerbosityLevel = 0;
@@ -88,6 +96,7 @@ private:
bool IsDatabaseSet = false;
bool IsIamEndpointSet = false;
bool IsCaCertsFileSet = false;
+ bool IsClientCertFileSet = false;
bool IsAuthSet = false;
};
diff --git a/ydb/public/lib/ydb_cli/common/cert_format_converter.cpp b/ydb/public/lib/ydb_cli/common/cert_format_converter.cpp
new file mode 100644
index 0000000000..ac9186460b
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/cert_format_converter.cpp
@@ -0,0 +1,207 @@
+#include "cert_format_converter.h"
+
+#include <ydb/public/lib/ydb_cli/common/common.h>
+
+#include <util/generic/strbuf.h>
+#include <util/stream/output.h>
+
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pkcs12.h>
+#include <openssl/pem.h>
+#include <openssl/x509.h>
+
+namespace NYdb::NConsoleClient {
+
+namespace {
+
+class TOpenSslObjectFree {
+public:
+ TOpenSslObjectFree() = default;
+ TOpenSslObjectFree(const TOpenSslObjectFree&) = default;
+ TOpenSslObjectFree(TOpenSslObjectFree&&) = default;
+
+ TOpenSslObjectFree& operator=(const TOpenSslObjectFree&) = default;
+ TOpenSslObjectFree& operator=(TOpenSslObjectFree&&) = default;
+
+ void operator()(X509* x509) const {
+ X509_free(x509);
+ }
+
+ void operator()(BIO* bio) const {
+ BIO_free(bio);
+ }
+
+ void operator()(EVP_PKEY* pkey) const {
+ EVP_PKEY_free(pkey);
+ }
+
+ void operator()(PKCS12* p) const {
+ PKCS12_free(p);
+ }
+};
+
+struct TPasswordProcessor {
+ TString Password;
+ bool IsInteractiveMode;
+ bool PasswordIsUsed = false;
+
+ const TString& GetPassword() {
+ if (PasswordIsUsed || Password || !IsInteractiveMode) {
+ return Password;
+ }
+ PasswordIsUsed = true;
+ Cerr << "Enter private key password: ";
+ return Password = InputPassword();
+ }
+
+ static int PasswordCallback(char* buf, int size, int rwflag, void* userdata) {
+ Y_UNUSED(rwflag);
+ const TString& password = reinterpret_cast<TPasswordProcessor*>(userdata)->GetPassword();
+ size_t s = 0;
+ if (s = Min<size_t>(password.size(), size)) {
+ memcpy(buf, password.c_str(), s);
+ }
+ return static_cast<int>(s);
+ }
+};
+
+using TX509Ptr = std::unique_ptr<X509, TOpenSslObjectFree>;
+using TBioPtr = std::unique_ptr<BIO, TOpenSslObjectFree>;
+using TEvpPKeyPtr = std::unique_ptr<EVP_PKEY, TOpenSslObjectFree>;
+using TPkcs12Ptr = std::unique_ptr<PKCS12, TOpenSslObjectFree>;
+
+TString ToString(const TBioPtr& bio) {
+ char* buf;
+ size_t len = BIO_get_mem_data(bio.get(), &buf);
+ return TString(buf, len);
+}
+
+TBioPtr ToBio(const TStringBuf& buf) {
+ return TBioPtr(BIO_new_mem_buf(buf.data(), buf.size()));
+}
+
+TBioPtr CreateMemBio() {
+ return TBioPtr(BIO_new(BIO_s_mem()));
+}
+
+TString GetLastOpenSslError() {
+ TBioPtr bio = CreateMemBio();
+ ERR_print_errors(bio.get());
+ return ToString(bio);
+}
+
+TString GetOpenSslErrorText(int errorCode) {
+ TStringBuilder result;
+ result << "Code " << errorCode;
+ if (TString err = GetLastOpenSslError()) {
+ result << ": " << err;
+ }
+ return std::move(result);
+}
+
+TX509Ptr TryReadCertFromPEM(const TString& cert) {
+ auto bio = ToBio(cert);
+ return TX509Ptr(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
+}
+
+TEvpPKeyPtr TryReadPrivateKeyFromPEM(const TString& key, TPasswordProcessor& password) {
+ auto bio = ToBio(key);
+ return TEvpPKeyPtr(PEM_read_bio_PrivateKey(bio.get(), nullptr, &TPasswordProcessor::PasswordCallback, &password));
+}
+
+TString EncodeCertToPEM(const TX509Ptr& cert) {
+ TBioPtr bio = CreateMemBio();
+
+ if (int err = PEM_write_bio_X509(bio.get(), cert.get()); err <= 0) {
+ throw yexception() << "Failed to write certificate: " << GetOpenSslErrorText(err);
+ }
+ return ToString(bio);
+}
+
+TString EncodePrivateKeyToPEM(const TEvpPKeyPtr& key) {
+ TBioPtr bio = CreateMemBio();
+
+ if (int err = PEM_write_bio_PrivateKey(bio.get(), key.get(), nullptr, nullptr, 0, nullptr, nullptr); err <= 0) {
+ throw yexception() << "Failed to write private key: " << GetOpenSslErrorText(err);
+ }
+ return ToString(bio);
+}
+
+TPkcs12Ptr TryReadPkcs12(const TString& cert) {
+ auto bio = ToBio(cert);
+ TPkcs12Ptr pkcs12(d2i_PKCS12_bio(bio.get(), nullptr));
+ return pkcs12;
+}
+
+bool IsPasswordProblem() {
+ while (unsigned long err = ERR_get_error()) {
+ if (ERR_GET_REASON(err) == PKCS12_R_MAC_VERIFY_FAILURE) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void ParsePkcs12(const TPkcs12Ptr& pkcs12, TX509Ptr& cert, TEvpPKeyPtr& key, TPasswordProcessor& password) {
+ X509* certPtr = nullptr;
+ EVP_PKEY* keyPtr = nullptr;
+ int err = PKCS12_parse(pkcs12.get(), password.Password.c_str(), &keyPtr, &certPtr, nullptr);
+ if (!password.Password && IsPasswordProblem()) { // Password in not correct, try to ask password from user (if possible)
+ if (password.GetPassword()) { // Got new password
+ err = PKCS12_parse(pkcs12.get(), password.Password.c_str(), &keyPtr, &certPtr, nullptr);
+ }
+ }
+ if (err <= 0) {
+ return;
+ }
+ if (certPtr) {
+ cert.reset(certPtr);
+ }
+ if (keyPtr) {
+ key.reset(keyPtr);
+ }
+}
+
+} // anonymous
+
+std::pair<TString, TString> ConvertCertToPEM(
+ const TString& certificate,
+ const TString& privateKey,
+ const TString& privateKeyPassword,
+ bool isInteractiveMode
+)
+{
+ TPasswordProcessor password = {
+ .Password = privateKeyPassword,
+ .IsInteractiveMode = isInteractiveMode,
+ };
+
+ TString certResult, privateKeyResult;
+ TX509Ptr cert;
+ TEvpPKeyPtr key;
+ if (certificate) {
+ cert = TryReadCertFromPEM(certificate);
+ }
+
+ if (!key && privateKey) {
+ key = TryReadPrivateKeyFromPEM(privateKey, password);
+ }
+
+ if (!key && certificate) { // There may be a concatenated file with both cert and key
+ key = TryReadPrivateKeyFromPEM(certificate, password);
+ }
+
+ if (!cert || !key) {
+ if (TPkcs12Ptr pkcs12 = TryReadPkcs12(certificate)) {
+ ParsePkcs12(pkcs12, cert, key, password);
+ }
+ }
+
+ if (cert && key) {
+ return {EncodeCertToPEM(cert), EncodePrivateKeyToPEM(key)};
+ }
+ throw yexception() << "Failed to parse client certificate and/or private key";
+}
+
+} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/common/cert_format_converter.h b/ydb/public/lib/ydb_cli/common/cert_format_converter.h
new file mode 100644
index 0000000000..55babfdf0c
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/cert_format_converter.h
@@ -0,0 +1,36 @@
+#pragma once
+#include "interactive.h"
+
+#include <util/generic/string.h>
+
+#include <utility>
+
+namespace NYdb::NConsoleClient {
+
+// Checks current certificate/private key format.
+// Supported formats are: PEM and PKCS#12.
+// If certificate/private key are not encoded in PEM format,
+// or if private key is encrypted with password phrase,
+// tries to convert them to PEM without password protection
+// (grpc lib requires client certificate to be encoded in this way).
+// In interactive mode asks user to enter password if it is required and not set.
+// In non-interactive mode fails in this case.
+// Returns <certificate, private key>.
+//
+// So, supported combinations are:
+// 1. Cert and key in PEM format, key is not encrypted.
+// Just return cert and key as is.
+// 2. Cert and key in PEM format, key is encrypted.
+// Reencode private key to be suitable for grpc lib (not encrypted). Maybe ask password from user interactively.
+// 3. Cert is in PKCS#12 encoding, unencrypted key is embedded into it.
+// Reencode cert and key to PEM
+// 4. Cert is in PKCS#12 encoding, encrypted key is embedded into it.
+// Reencode cert and key to PEM without encryption. Maybe ask password from user interactively.
+std::pair<TString, TString> ConvertCertToPEM(
+ const TString& certificate,
+ const TString& privateKey,
+ const TString& privateKeyPassword,
+ bool isInteractiveMode = IsStdinInteractive()
+);
+
+} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/common/cert_format_converter_ut.cpp b/ydb/public/lib/ydb_cli/common/cert_format_converter_ut.cpp
new file mode 100644
index 0000000000..d503700239
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/cert_format_converter_ut.cpp
@@ -0,0 +1,105 @@
+#include "cert_format_converter.h"
+
+#include <library/cpp/testing/common/env.h>
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/stream/file.h>
+
+namespace NYdb::NConsoleClient {
+
+namespace {
+
+TString GetDataPath(const TString& path) {
+ return ArcadiaFromCurrentLocation(__SOURCE_FILE__, "ut/test_data/" + path);
+}
+
+TString GetDataByPath(const TString& path) {
+ if (path) {
+ return TFileInput(GetDataPath(path)).ReadAll();
+ }
+ return {};
+}
+
+const TString PASSWORD = "p@ssw0rd";
+
+} // anonymous
+
+Y_UNIT_TEST_SUITE(CertFormatConverter) {
+ void ValidateParse(const TString& certPath, const TString& keyPath, bool withPassword) {
+ TString certContent = GetDataByPath(certPath);
+ TString keyContent = GetDataByPath(keyPath);
+ TString cert, key;
+ if (withPassword) {
+ // Does not parse without password
+ // And also does not ask it, because we are in noninteractive mode
+ UNIT_ASSERT_EXCEPTION_C(
+ std::tie(cert, key) = ConvertCertToPEM(certContent, keyContent, "", false),
+ yexception,
+ "Cert: " << certPath << ". Key: " << keyPath);
+
+ UNIT_ASSERT_NO_EXCEPTION_C(
+ std::tie(cert, key) = ConvertCertToPEM(certContent, keyContent, PASSWORD, false),
+ "Cert: " << certPath << ". Key: " << keyPath);
+ } else {
+ UNIT_ASSERT_NO_EXCEPTION_C(
+ std::tie(cert, key) = ConvertCertToPEM(certContent, keyContent, "", false),
+ "Cert: " << certPath << ". Key: " << keyPath);
+ }
+
+ UNIT_ASSERT_STRING_CONTAINS_C(cert, "-----BEGIN CERTIFICATE-----", "Cert: " << certPath << ". Key: " << keyPath);
+ UNIT_ASSERT_STRING_CONTAINS_C(key, "-----BEGIN PRIVATE KEY-----", "Cert: " << certPath << ". Key: " << keyPath);
+
+ // Check that returned cert/key can be parsed
+ UNIT_ASSERT_NO_EXCEPTION_C(
+ ConvertCertToPEM(cert, key, "", false),
+ "Cert: " << certPath << ". Key: " << keyPath);
+ }
+
+ Y_UNIT_TEST(ParseFromPEM) {
+ ValidateParse("ec-cert.pem", "ec-private-key.pem", false);
+ ValidateParse("ec-cert.p12", "", false);
+ ValidateParse("ec-all.pem", "ec-all.pem", false);
+ ValidateParse("ec-all.pem", "", false);
+ ValidateParse("rsa-cert.pem", "rsa-private-key.pem", false);
+ ValidateParse("rsa-cert.p12", "", false);
+ ValidateParse("rsa-all.pem", "rsa-all.pem", false);
+ ValidateParse("rsa-all.pem", "", false);
+
+ ValidateParse("ec-cert.pem", "ec-private-key-pwd.pem", true);
+ ValidateParse("ec-cert-pwd.p12", "", true);
+ ValidateParse("ec-all-pwd.pem", "ec-all-pwd.pem", true);
+ ValidateParse("ec-all-pwd.pem", "", true);
+ ValidateParse("rsa-cert.pem", "rsa-private-key-pwd.pem", true);
+ ValidateParse("rsa-cert-pwd.p12", "", true);
+ ValidateParse("rsa-all-pwd.pem", "rsa-all-pwd.pem", true);
+ ValidateParse("rsa-all-pwd.pem", "", true);
+ }
+
+ Y_UNIT_TEST(InvalidKey) {
+ TString certContent = GetDataByPath("ec-cert.pem");
+ TString keyContent = "invalid";
+ UNIT_ASSERT_EXCEPTION(
+ ConvertCertToPEM(certContent, keyContent, "", false),
+ yexception);
+ }
+
+ Y_UNIT_TEST(InvalidCert) {
+ TString certContent = "invalid";
+ TString keyContent = GetDataByPath("rsa-private-key.pem");
+ UNIT_ASSERT_EXCEPTION(
+ ConvertCertToPEM(certContent, keyContent, "", false),
+ yexception);
+ }
+
+ Y_UNIT_TEST(InvalidKeyTakeFromCert) {
+ TString certContent = GetDataByPath("ec-all.pem");
+ TString keyContent = "invalid";
+
+ UNIT_ASSERT_NO_EXCEPTION(ConvertCertToPEM(certContent, keyContent, "", false));
+
+ certContent = GetDataByPath("ec-cert-pwd.p12");
+ UNIT_ASSERT_NO_EXCEPTION(ConvertCertToPEM(certContent, keyContent, PASSWORD, false));
+ }
+}
+
+} // namespace NYdb::NConsoleClient
diff --git a/ydb/public/lib/ydb_cli/common/command.h b/ydb/public/lib/ydb_cli/common/command.h
index 324a729c5c..d3ea848edf 100644
--- a/ydb/public/lib/ydb_cli/common/command.h
+++ b/ydb/public/lib/ydb_cli/common/command.h
@@ -110,6 +110,18 @@ public:
TString Database;
TString CaCerts;
TString CaCertsFile;
+ TString ClientCert;
+ TString ClientCertPrivateKey;
+ TString ClientCertPrivateKeyPassword;
+ TString ClientCertFile;
+ TString ClientCertPrivateKeyFile;
+ TString ClientCertPrivateKeyPasswordFile;
+
+ // Client cert initialization.
+ // Parses certificate from dirrefent formats.
+ // Can ask for password if private key is protected with password and it is not set in options.
+ void InitClientCert();
+
TMap<TString, TVector<TConnectionParam>> ConnectionParams;
bool EnableSsl = false;
bool IsNetworkIntensive = false;
diff --git a/ydb/public/lib/ydb_cli/common/root.cpp b/ydb/public/lib/ydb_cli/common/root.cpp
index 3ebbbd2686..790967a560 100644
--- a/ydb/public/lib/ydb_cli/common/root.cpp
+++ b/ydb/public/lib/ydb_cli/common/root.cpp
@@ -1,4 +1,5 @@
#include "root.h"
+#include "cert_format_converter.h"
#include <util/folder/path.h>
#include <util/folder/dirut.h>
#include <util/string/strip.h>
@@ -22,9 +23,19 @@ void TClientCommandRootBase::Config(TConfig& config) {
opts.AddLongOption('t', "time", "Show request execution time").NoArgument().SetFlag(&TimeRequests);
opts.AddLongOption('o', "progress", "Show progress of long requests").NoArgument().SetFlag(&ProgressRequests);
opts.AddLongOption("ca-file",
- "Path to a file containing the PEM encoding of the server root certificates for tls connections.\n"
+ "File containing PEM encoded root certificates for SSL/TLS connections.\n"
"If this parameter is empty, the default roots will be used.")
.RequiredArgument("PATH").StoreResult(&CaCertsFile);
+ opts.AddLongOption("client-cert-file",
+ "File containing client certificate for SSL/TLS connections (PKCS#12 or PEM-encoded).")
+ .RequiredArgument("PATH").StoreResult(&ClientCertFile);
+ opts.AddLongOption("client-cert-key-file",
+ "File containing PEM encoded client certificate private key for SSL/TLS connections.")
+ .RequiredArgument("PATH").StoreResult(&ClientCertPrivateKeyFile);
+ opts.AddLongOption("client-cert-key-password-file",
+ "File containing password for client certificate private key (if key is encrypted). "
+ "If key file is encrypted, but this option is not set, password will be asked interactively.")
+ .RequiredArgument("PATH").StoreResult(&ClientCertPrivateKeyPasswordFile);
opts.SetCustomUsage(config.ArgV[0]);
config.SetFreeArgsMin(1);
@@ -92,6 +103,27 @@ void TClientCommandRootBase::ParseCaCerts(TConfig& config) {
config.CaCerts = ReadFromFile(CaCertsFile, "CA certificates");
}
+void TClientCommandRootBase::ParseClientCert(TConfig& config) {
+ if (ClientCertFile.empty()) {
+ return;
+ }
+ if (!config.EnableSsl) {
+ throw TMisuseException()
+ << "\"client-cert-file\" option provided for a non-ssl connection. Use grpcs:// prefix for host to connect using SSL.";
+ }
+ config.ClientCert = ReadFromFile(ClientCertFile, "Client certificate");
+ if (ClientCertPrivateKeyFile) {
+ config.ClientCertPrivateKey = ReadFromFile(ClientCertPrivateKeyFile, "Client certificate private key");
+ }
+ if (ClientCertPrivateKeyPasswordFile) {
+ config.ClientCertPrivateKeyPassword = ReadFromFile(ClientCertPrivateKeyPasswordFile, "Client certificate private key password");
+ }
+
+ // Convert certificates from PKCS#12 to PEM or encrypted private key to nonencrypted
+ // May ask for password
+ std::tie(config.ClientCert, config.ClientCertPrivateKey) = ConvertCertToPEM(config.ClientCert, config.ClientCertPrivateKey, config.ClientCertPrivateKeyPassword);
+}
+
void TClientCommandRootBase::ParseCredentials(TConfig& config) {
ParseToken(Token, TokenFile, "YDB_TOKEN", true);
if (!Token.empty()) {
diff --git a/ydb/public/lib/ydb_cli/common/root.h b/ydb/public/lib/ydb_cli/common/root.h
index 6ccd4cce46..12dccf1348 100644
--- a/ydb/public/lib/ydb_cli/common/root.h
+++ b/ydb/public/lib/ydb_cli/common/root.h
@@ -17,6 +17,9 @@ public:
TString Token;
TString TokenFile;
TString CaCertsFile;
+ TString ClientCertFile;
+ TString ClientCertPrivateKeyFile;
+ TString ClientCertPrivateKeyPasswordFile;
virtual void Config(TConfig& config) override;
virtual void Parse(TConfig& config) override;
@@ -27,6 +30,7 @@ protected:
void ParseToken(TString& token, TString& tokenFile, const TString& envName, bool useDefaultToken = false);
bool ParseProtocol(TConfig& config, TString& message);
virtual void ParseCaCerts(TConfig& config);
+ virtual void ParseClientCert(TConfig& config);
virtual void ParseCredentials(TConfig& config);
virtual void ParseAddress(TConfig& config) = 0;
};
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ca-private-key.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ca-private-key.pem
new file mode 100644
index 0000000000..f565c71800
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ca-private-key.pem
@@ -0,0 +1,6 @@
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDAK2UOfGT0ZDbXUT3qO351S4UiP2bqxD/YrGSS0VYB1rlK3sey7FS0G
+DaF8/es/NeagBwYFK4EEACKhZANiAAT+vF2+8tuWAlb1+ByT9F4cygVYlOvfsiU/
+lW1TQwZZXV6tTWqPqCwwjJ96l11T62Dc4tfLlBSea6Sx6k1JvbjXMeYICdeIASQO
+mTF/d+V3FpC7fJ7L0RhfFr9v+Dj1OaE=
+-----END EC PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/cert-ext.cfg b/ydb/public/lib/ydb_cli/common/ut/test_data/cert-ext.cfg
new file mode 100644
index 0000000000..86b4bc9c44
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/cert-ext.cfg
@@ -0,0 +1,10 @@
+basicConstraints = CA:FALSE
+nsCertType = server, client
+nsComment = "Test certificate"
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid,issuer:always
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+extendedKeyUsage = serverAuth, clientAuth
+subjectAltName = @alt_names
+[alt_names]
+DNS.1 = host.ydb.net
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/cert.cfg b/ydb/public/lib/ydb_cli/common/ut/test_data/cert.cfg
new file mode 100644
index 0000000000..b9eb31ba74
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/cert.cfg
@@ -0,0 +1,15 @@
+[req]
+default_md = sha512
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+[req]
+distinguished_name = test_host
+req_extensions = req_ext
+prompt = no
+[test_host]
+O = YDB
+CN = host.ydb.net
+[req_ext]
+subjectAltName = @alt_names
+[alt_names]
+DNS.1 = host.ydb.net
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-all-pwd.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-all-pwd.pem
new file mode 100644
index 0000000000..4fea9f97fd
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-all-pwd.pem
@@ -0,0 +1,27 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,972C596A7496261BE01ECBC6D8A5034D
+
+xgqlS5qHKMwyBK/8m+xKTcPNvMK+8ieY688cr+iG28x86puF8AQNGNiSJBHSkWrK
+IQhAV53UhkiX1rCjYFYrri84qcwu5FJ6DARJojvhSic3W1pBsbNN0WKmFL/3XZ8K
+bMM95QxqhjQDnEvXuxmmJwA+VFa7K+tjlioVaSu1oZrUKLiUzXu3hRrPnYiAU9GJ
+TZFZZSseFV6Eyn3vWntOt33f9VbcHICp/HVz/K3kUhE=
+-----END EC PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAlqgAwIBAgITfoCHUO2Lyx7Ge7aitBhp8+57FTAKBggqhkjOPQQDBDBF
+MQwwCgYDVQQKDANZREIxGTAXBgNVBAMMEFlEQiBUZXN0IFJvb3QgQ0ExGjAYBgkq
+hkiG9w0BCQEWC3lkYkB5ZGIueWRiMB4XDTI1MDIyNzE1MzA0MFoXDTI2MDIyNzE1
+MzA0MFowJTEMMAoGA1UECgwDWURCMRUwEwYDVQQDDAxob3N0LnlkYi5uZXQwdjAQ
+BgcqhkjOPQIBBgUrgQQAIgNiAATS8Pia9B3jaelvF3crFc5d6HSqqGwPzvEACI9T
+iP4a/hnqhpZEVI9kk7RWmiV7xG2PL61VHS3WtguAF/RwlFJQJ2tscWYx0qtjcAdI
+i3xui8PT6mmHG/I0CbFZ4uAa6/qjggEqMIIBJjAJBgNVHRMEAjAAMBEGCWCGSAGG
++EIBAQQEAwIGwDAfBglghkgBhvhCAQ0EEhYQVGVzdCBjZXJ0aWZpY2F0ZTAdBgNV
+HQ4EFgQUT12c+6pblN4Hm2ZZk4WBOe7uaTgwgYAGA1UdIwR5MHeAFMm4UhwU9rl3
+UYH3NPzzFMl1OIOboUmkRzBFMQwwCgYDVQQKDANZREIxGTAXBgNVBAMMEFlEQiBU
+ZXN0IFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC3lkYkB5ZGIueWRighRYDsITD3rH
+t3W4BIPBfQDGprUiBTALBgNVHQ8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
+CCsGAQUFBwMCMBcGA1UdEQQQMA6CDGhvc3QueWRiLm5ldDAKBggqhkjOPQQDBANo
+ADBlAjEA2c9hW4aMJ1agvFZlyBVXp8kOrZwNl/ycCKBdJ6XsxBeqHZJH84+p9hmq
+a1WwnE5dAjAsMGUiHFMZCPuFMt1ssOk1HrMgmv0pYXYPcFBElZZ58svFvog5gyhn
+GZ5s8IGptTY=
+-----END CERTIFICATE-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-all.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-all.pem
new file mode 100644
index 0000000000..2c405bb755
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-all.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAlqgAwIBAgITfoCHUO2Lyx7Ge7aitBhp8+57FTAKBggqhkjOPQQDBDBF
+MQwwCgYDVQQKDANZREIxGTAXBgNVBAMMEFlEQiBUZXN0IFJvb3QgQ0ExGjAYBgkq
+hkiG9w0BCQEWC3lkYkB5ZGIueWRiMB4XDTI1MDIyNzE1MzA0MFoXDTI2MDIyNzE1
+MzA0MFowJTEMMAoGA1UECgwDWURCMRUwEwYDVQQDDAxob3N0LnlkYi5uZXQwdjAQ
+BgcqhkjOPQIBBgUrgQQAIgNiAATS8Pia9B3jaelvF3crFc5d6HSqqGwPzvEACI9T
+iP4a/hnqhpZEVI9kk7RWmiV7xG2PL61VHS3WtguAF/RwlFJQJ2tscWYx0qtjcAdI
+i3xui8PT6mmHG/I0CbFZ4uAa6/qjggEqMIIBJjAJBgNVHRMEAjAAMBEGCWCGSAGG
++EIBAQQEAwIGwDAfBglghkgBhvhCAQ0EEhYQVGVzdCBjZXJ0aWZpY2F0ZTAdBgNV
+HQ4EFgQUT12c+6pblN4Hm2ZZk4WBOe7uaTgwgYAGA1UdIwR5MHeAFMm4UhwU9rl3
+UYH3NPzzFMl1OIOboUmkRzBFMQwwCgYDVQQKDANZREIxGTAXBgNVBAMMEFlEQiBU
+ZXN0IFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC3lkYkB5ZGIueWRighRYDsITD3rH
+t3W4BIPBfQDGprUiBTALBgNVHQ8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
+CCsGAQUFBwMCMBcGA1UdEQQQMA6CDGhvc3QueWRiLm5ldDAKBggqhkjOPQQDBANo
+ADBlAjEA2c9hW4aMJ1agvFZlyBVXp8kOrZwNl/ycCKBdJ6XsxBeqHZJH84+p9hmq
+a1WwnE5dAjAsMGUiHFMZCPuFMt1ssOk1HrMgmv0pYXYPcFBElZZ58svFvog5gyhn
+GZ5s8IGptTY=
+-----END CERTIFICATE-----
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDBCn8VZ3bNXU7QQV1JeKmahm1sbcy2gctHUuVG9bN28Hyh1kuLiOZDr
+RntgGfEa1QGgBwYFK4EEACKhZANiAATS8Pia9B3jaelvF3crFc5d6HSqqGwPzvEA
+CI9TiP4a/hnqhpZEVI9kk7RWmiV7xG2PL61VHS3WtguAF/RwlFJQJ2tscWYx0qtj
+cAdIi3xui8PT6mmHG/I0CbFZ4uAa6/o=
+-----END EC PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert-pwd.p12 b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert-pwd.p12
new file mode 100644
index 0000000000..c0c523cece
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert-pwd.p12
Binary files differ
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.p12 b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.p12
new file mode 100644
index 0000000000..65630d09b1
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.p12
Binary files differ
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.pem
new file mode 100644
index 0000000000..17f4b31ddd
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-cert.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAlqgAwIBAgITfoCHUO2Lyx7Ge7aitBhp8+57FTAKBggqhkjOPQQDBDBF
+MQwwCgYDVQQKDANZREIxGTAXBgNVBAMMEFlEQiBUZXN0IFJvb3QgQ0ExGjAYBgkq
+hkiG9w0BCQEWC3lkYkB5ZGIueWRiMB4XDTI1MDIyNzE1MzA0MFoXDTI2MDIyNzE1
+MzA0MFowJTEMMAoGA1UECgwDWURCMRUwEwYDVQQDDAxob3N0LnlkYi5uZXQwdjAQ
+BgcqhkjOPQIBBgUrgQQAIgNiAATS8Pia9B3jaelvF3crFc5d6HSqqGwPzvEACI9T
+iP4a/hnqhpZEVI9kk7RWmiV7xG2PL61VHS3WtguAF/RwlFJQJ2tscWYx0qtjcAdI
+i3xui8PT6mmHG/I0CbFZ4uAa6/qjggEqMIIBJjAJBgNVHRMEAjAAMBEGCWCGSAGG
++EIBAQQEAwIGwDAfBglghkgBhvhCAQ0EEhYQVGVzdCBjZXJ0aWZpY2F0ZTAdBgNV
+HQ4EFgQUT12c+6pblN4Hm2ZZk4WBOe7uaTgwgYAGA1UdIwR5MHeAFMm4UhwU9rl3
+UYH3NPzzFMl1OIOboUmkRzBFMQwwCgYDVQQKDANZREIxGTAXBgNVBAMMEFlEQiBU
+ZXN0IFJvb3QgQ0ExGjAYBgkqhkiG9w0BCQEWC3lkYkB5ZGIueWRighRYDsITD3rH
+t3W4BIPBfQDGprUiBTALBgNVHQ8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
+CCsGAQUFBwMCMBcGA1UdEQQQMA6CDGhvc3QueWRiLm5ldDAKBggqhkjOPQQDBANo
+ADBlAjEA2c9hW4aMJ1agvFZlyBVXp8kOrZwNl/ycCKBdJ6XsxBeqHZJH84+p9hmq
+a1WwnE5dAjAsMGUiHFMZCPuFMt1ssOk1HrMgmv0pYXYPcFBElZZ58svFvog5gyhn
+GZ5s8IGptTY=
+-----END CERTIFICATE-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pkcs8.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pkcs8.pem
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pkcs8.pem
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pwd.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pwd.pem
new file mode 100644
index 0000000000..2f2c7336b4
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key-pwd.pem
@@ -0,0 +1,9 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,972C596A7496261BE01ECBC6D8A5034D
+
+xgqlS5qHKMwyBK/8m+xKTcPNvMK+8ieY688cr+iG28x86puF8AQNGNiSJBHSkWrK
+IQhAV53UhkiX1rCjYFYrri84qcwu5FJ6DARJojvhSic3W1pBsbNN0WKmFL/3XZ8K
+bMM95QxqhjQDnEvXuxmmJwA+VFa7K+tjlioVaSu1oZrUKLiUzXu3hRrPnYiAU9GJ
+TZFZZSseFV6Eyn3vWntOt33f9VbcHICp/HVz/K3kUhE=
+-----END EC PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key.pem
new file mode 100644
index 0000000000..bb0c073564
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/ec-private-key.pem
@@ -0,0 +1,6 @@
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDBCn8VZ3bNXU7QQV1JeKmahm1sbcy2gctHUuVG9bN28Hyh1kuLiOZDr
+RntgGfEa1QGgBwYFK4EEACKhZANiAATS8Pia9B3jaelvF3crFc5d6HSqqGwPzvEA
+CI9TiP4a/hnqhpZEVI9kk7RWmiV7xG2PL61VHS3WtguAF/RwlFJQJ2tscWYx0qtj
+cAdIi3xui8PT6mmHG/I0CbFZ4uAa6/o=
+-----END EC PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/generate_certs.sh b/ydb/public/lib/ydb_cli/common/ut/test_data/generate_certs.sh
new file mode 100755
index 0000000000..5082f8450c
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/generate_certs.sh
@@ -0,0 +1,49 @@
+#! /usr/bin/env bash
+set -e
+set -x
+
+rm *.pem
+rm *.p12
+
+# Root CA
+openssl ecparam -name secp384r1 -genkey -noout -out ca-private-key.pem
+openssl req -x509 -new -nodes -key ca-private-key.pem -sha512 -days 1826 -subj '/O=YDB/CN=YDB Test Root CA/emailAddress=ydb@ydb.ydb' -out root-ca.pem
+
+# Generate EC PEM cert without password
+openssl ecparam -name secp384r1 -genkey -noout -out ec-private-key.pem
+openssl req -new -sha512 -nodes -key ec-private-key.pem -config cert.cfg -out ec-cert.csr
+openssl x509 -req -sha512 -days 365 -in ec-cert.csr -CA root-ca.pem -CAkey ca-private-key.pem -CAcreateserial -extfile cert-ext.cfg -out ec-cert.pem
+
+# Generate RSA PEM cert without password
+openssl genrsa -out rsa-private-key.pem 2048
+openssl req -new -sha512 -nodes -key rsa-private-key.pem -config cert.cfg -out rsa-cert.csr
+openssl x509 -req -sha512 -days 365 -in rsa-cert.csr -CA root-ca.pem -CAkey ca-private-key.pem -CAcreateserial -extfile cert-ext.cfg -out rsa-cert.pem
+
+# Generate EC PEM key with password
+openssl ec -in ec-private-key.pem -aes256 -passout 'pass:p@ssw0rd' -out ec-private-key-pwd.pem
+
+# Generate RSA PEM key with password
+openssl rsa -in rsa-private-key.pem -aes256 -passout 'pass:p@ssw0rd' -out rsa-private-key-pwd.pem
+
+# Generate EC PKCS#12 cert without password
+openssl pkcs12 -export -in ec-cert.pem -inkey ec-private-key.pem -name 'Test cert' -passout 'pass:' -out ec-cert.p12
+
+# Generate RSA PKCS#12 cert without password
+openssl pkcs12 -export -in rsa-cert.pem -inkey rsa-private-key.pem -name 'Test cert' -passout 'pass:' -out rsa-cert.p12
+
+# Generate EC PKCS#12 cert with password
+openssl pkcs12 -export -in ec-cert.pem -inkey ec-private-key.pem -name 'Test cert' -passout 'pass:p@ssw0rd' -out ec-cert-pwd.p12
+
+# Generate RSA PKCS#12 cert with password
+openssl pkcs12 -export -in rsa-cert.pem -inkey rsa-private-key.pem -name 'Test cert' -passout 'pass:p@ssw0rd' -out rsa-cert-pwd.p12
+
+# Concat EC PEM cert and private key into one file
+cat ec-cert.pem ec-private-key.pem > ec-all.pem
+cat ec-private-key-pwd.pem ec-cert.pem > ec-all-pwd.pem
+
+# Concat RSA PEM cert and private key into one file
+cat rsa-cert.pem rsa-private-key.pem > rsa-all.pem
+cat rsa-private-key-pwd.pem rsa-cert.pem > rsa-all-pwd.pem
+
+# Cleanup
+rm *.csr
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/root-ca.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/root-ca.pem
new file mode 100644
index 0000000000..be4d4a71bc
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/root-ca.pem
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICHDCCAaKgAwIBAgIUWA7CEw96x7d1uASDwX0Axqa1IgUwCgYIKoZIzj0EAwQw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYjAeFw0yNTAyMjcxNTMwNDBaFw0zMDAyMjcx
+NTMwNDBaMEUxDDAKBgNVBAoMA1lEQjEZMBcGA1UEAwwQWURCIFRlc3QgUm9vdCBD
+QTEaMBgGCSqGSIb3DQEJARYLeWRiQHlkYi55ZGIwdjAQBgcqhkjOPQIBBgUrgQQA
+IgNiAAT+vF2+8tuWAlb1+ByT9F4cygVYlOvfsiU/lW1TQwZZXV6tTWqPqCwwjJ96
+l11T62Dc4tfLlBSea6Sx6k1JvbjXMeYICdeIASQOmTF/d+V3FpC7fJ7L0RhfFr9v
++Dj1OaGjUzBRMB0GA1UdDgQWBBTJuFIcFPa5d1GB9zT88xTJdTiDmzAfBgNVHSME
+GDAWgBTJuFIcFPa5d1GB9zT88xTJdTiDmzAPBgNVHRMBAf8EBTADAQH/MAoGCCqG
+SM49BAMEA2gAMGUCMGKi3yp8yUAvsFEhwPzJtzRZEhmrxexO5hZY0sO0aEZ/F36n
+g4MwKI77uylSp3zZ7QIxAN/b0ioIJEDMG4Z4iu6LwkMf7iM41YnH2keE+U6tt3YP
+UoZPGRIcOT7pdWo0qw+zhQ==
+-----END CERTIFICATE-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all-pwd.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all-pwd.pem
new file mode 100644
index 0000000000..7263b191a6
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all-pwd.pem
@@ -0,0 +1,51 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIj9j7mZP26RoCAggA
+MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCtR2G2S3gQV05HBrC2K8zJBIIE
+0NHU/ROqMHHu84Td4a7FdymDQzem/fnioBuQadWLGvQMs/rbqGm8iUvMFis3M5Aq
+wdHKUqO6UMKL4W+HCJtF2C/qFYkeWeAGOcddcuKbFSeOqTiiRFREpcX+MrFG9AJr
+RVVmjCo3NHWBWw17plhP2fG/wUoHTnFbN9KgwV38mWF0ixe2InexQETFZu3bgIXT
+VTV6+yVC++pDxhKFGmbzWu+rN3TwMAubpLy18c49BdYjmWvKdATcJ200SH9Lif4I
+9FSs745hJV0qNrCjRoZCji+pc1UoW+MNoj2F+m8kJBb0i4+LhaAtEOCBtj+V4oQJ
+dI38DBaPKwQS6LqczHmM4td93MyA44ubHzkkT2tMTmC5iPDIcG9599Idl5i6F0ZB
+99789+pei0IPlV0Kl2Z72SxdhOadlIeE+syeO1Jon0aP3b93xiodlvo5glEHNkAJ
+vmsJXaf1DReaIuDG+l8wtsKpvHQgHeIIRuTN4frned9X6XxwzvzLXWpBoL4q7M8A
+HPuPPrLyCpCh2wTbp9ukdE5zVPfCExduZ6b5atk5Wgxt7P1HDNp/nwHRG7TJbge6
+Krbi6OPwyU7xY8LiOwkhec4AzNelPNgaYvF1VgQtA0XkKz75cMv1ikwF2/OVnpFI
+U3QIEAvZAzxGZkbdqsGeboabtQ8vmY96QKqKBs3OIhEYq0OreA7YnQzppcCFEW3X
+Nt9wA06rBZRumx2MbZQ5NzOgEYVSXBcapA/yYR8wxyLKK/LqRvZXTanw3PojbooU
+zGXGxqTyYXPimSOlfgK9aRTBFsj0TGGAIbXZehTj36cNKoV0o9V912yRb7VpJIZY
+bRGHAbYpvOHkjvC6pi5sRDACkyma2H53uEkvZGMOva0Xfq/iDszeUNHeFAauMDvR
+OjdfXcJaUeO+PwUNyFpVZnmsQzsfU6FvfNcpiak6kQb2h2OlTXV1LQzCOTbUst6N
+fVv78wY+HlF6BG+pR/yWZEJvPQ0ui51v4FY+xK0/I0+so2lk2Pj3xSq2S+eqC56X
+s4CAPYzMfIZZBUhpBXeVjnMR2REjvsixoF6fghJso70aIhmbWvXGSw2LA07WRrmo
+zWmhGIJ/egBSPU8hb4XG7ZiybyFgAleyUFl5laI3F94zOAixsSj6o2FoiAFeiKN9
+hZevO27Aino/Ep6+fAJAIG6oi5F/svchrLtn0Mdr0iTU/jKbY1F151Vr6incv81f
+KkWheXr+NO0VaS1Qnh64lWV2rSSwQunLSZNL4SlMWxMTZykfDcAbb6B2UuNRKGnp
+ryYLDfFfY2YFQjT+lG84hkYeEY5mKZ/FFO1Vu59YxHaJLMCvfAoxbwXAFUCeN4h+
+WY/gPLXJLXtqEtSg8RvCIjUmqfq72zpjuu/KxVbrEmXWLA8RE0X3UolZz+kWxpCA
+JETc9S5+iexMi0ns/Fe7y8thMxHMbg5aBSU4oq50lj+u5PdYyJI+EejrWobjl07T
+T5mJid9uEo4e7+HLqNz3ebMKpT87VLk/r4AoQwZ39P+4N2s7o/C6IBGpOqADHASA
+ieLWLoKt/O+o4vM0Kfpme3fHg2tzv9SeX0ECf6ZF0suq/Vs8xtWhuIMEhrM/tyFt
+pGLEHhTOuaRsoOy0mVeZYugqhx7v6mnzoYKWzoKMfrMS
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDhDCCAwmgAwIBAgIUOWjP4WDFqjNgjpi9+3RyLHVgVDowCgYIKoZIzj0EAwQw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYjAeFw0yNTAyMjcxNTMwNDBaFw0yNjAyMjcx
+NTMwNDBaMCUxDDAKBgNVBAoMA1lEQjEVMBMGA1UEAwwMaG9zdC55ZGIubmV0MIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0Gj5UzadnbHRckVcZI8BEFK
+DZZTjdVS5KXWQ3l/oNVEW5Ay+cYDQJWeO4+rdhXqCEsLPz0c6LCmZG8mBlz0Clp5
+aPkPsXOrZW+/qnW5hHq3BMJbyBLXcJjUXNShvbO+wO/mXN+RuJktxRxq9ZFc5Dxo
+23qSD2rnEsCO7+swXqzOI76x+a+euOMrWGN0vMaQBlfdkzE9FFhHNP3kUorSli5d
+IjF6TS2agmQM7VyKuodl2x/f/FeobzDqrTH5bpA0d1zj9maZaKpAC7Gt1Wkl7e7/
+48t3OOYpM4yNbzWUktLuEnMhySsCWZGu3hF3zSNpKaqh2ANlSK/J7KOC6VWN+wID
+AQABo4IBKjCCASYwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBsAwHwYJYIZI
+AYb4QgENBBIWEFRlc3QgY2VydGlmaWNhdGUwHQYDVR0OBBYEFHmk/j4vLRcLHPYk
+KGkjx141v5o9MIGABgNVHSMEeTB3gBTJuFIcFPa5d1GB9zT88xTJdTiDm6FJpEcw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYoIUWA7CEw96x7d1uASDwX0Axqa1IgUwCwYD
+VR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAXBgNVHREE
+EDAOggxob3N0LnlkYi5uZXQwCgYIKoZIzj0EAwQDaQAwZgIxAKF9+r38ij1iOdMl
+be+cT4r26S4Fo8rfDMP1YlIJ9dvlJjI7lPPW72441et4QvJEogIxAPp3GldSZFvu
+WFVUGSK0l2UNct5Rm5fegGKwMaunk/ZQO293JBc/AOU31FtgxcOLrg==
+-----END CERTIFICATE-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all.pem
new file mode 100644
index 0000000000..7c346bf7ca
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-all.pem
@@ -0,0 +1,49 @@
+-----BEGIN CERTIFICATE-----
+MIIDhDCCAwmgAwIBAgIUOWjP4WDFqjNgjpi9+3RyLHVgVDowCgYIKoZIzj0EAwQw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYjAeFw0yNTAyMjcxNTMwNDBaFw0yNjAyMjcx
+NTMwNDBaMCUxDDAKBgNVBAoMA1lEQjEVMBMGA1UEAwwMaG9zdC55ZGIubmV0MIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0Gj5UzadnbHRckVcZI8BEFK
+DZZTjdVS5KXWQ3l/oNVEW5Ay+cYDQJWeO4+rdhXqCEsLPz0c6LCmZG8mBlz0Clp5
+aPkPsXOrZW+/qnW5hHq3BMJbyBLXcJjUXNShvbO+wO/mXN+RuJktxRxq9ZFc5Dxo
+23qSD2rnEsCO7+swXqzOI76x+a+euOMrWGN0vMaQBlfdkzE9FFhHNP3kUorSli5d
+IjF6TS2agmQM7VyKuodl2x/f/FeobzDqrTH5bpA0d1zj9maZaKpAC7Gt1Wkl7e7/
+48t3OOYpM4yNbzWUktLuEnMhySsCWZGu3hF3zSNpKaqh2ANlSK/J7KOC6VWN+wID
+AQABo4IBKjCCASYwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBsAwHwYJYIZI
+AYb4QgENBBIWEFRlc3QgY2VydGlmaWNhdGUwHQYDVR0OBBYEFHmk/j4vLRcLHPYk
+KGkjx141v5o9MIGABgNVHSMEeTB3gBTJuFIcFPa5d1GB9zT88xTJdTiDm6FJpEcw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYoIUWA7CEw96x7d1uASDwX0Axqa1IgUwCwYD
+VR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAXBgNVHREE
+EDAOggxob3N0LnlkYi5uZXQwCgYIKoZIzj0EAwQDaQAwZgIxAKF9+r38ij1iOdMl
+be+cT4r26S4Fo8rfDMP1YlIJ9dvlJjI7lPPW72441et4QvJEogIxAPp3GldSZFvu
+WFVUGSK0l2UNct5Rm5fegGKwMaunk/ZQO293JBc/AOU31FtgxcOLrg==
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCvQaPlTNp2dsdF
+yRVxkjwEQUoNllON1VLkpdZDeX+g1URbkDL5xgNAlZ47j6t2FeoISws/PRzosKZk
+byYGXPQKWnlo+Q+xc6tlb7+qdbmEercEwlvIEtdwmNRc1KG9s77A7+Zc35G4mS3F
+HGr1kVzkPGjbepIPaucSwI7v6zBerM4jvrH5r5644ytYY3S8xpAGV92TMT0UWEc0
+/eRSitKWLl0iMXpNLZqCZAztXIq6h2XbH9/8V6hvMOqtMflukDR3XOP2ZploqkAL
+sa3VaSXt7v/jy3c45ikzjI1vNZSS0u4ScyHJKwJZka7eEXfNI2kpqqHYA2VIr8ns
+o4LpVY37AgMBAAECggEAFN3Q4M1MuYwc3884UWRi5vLLGWELo7iBfR3NrAz8SC78
+S5aYqyqLWpY118ucU5v6WHBu72GcXowh05V0Ro+sssrg58G3v54RCyvJ+0a3BvIB
+dnyZpvGvWwWnS0I527dZ/+jQWcB4vizmyTA//+sFz1rt9Svb1iAZUR9CJYiwIqVn
+xyDYbL3TzLacSs5iKzcbW4i6OeLphgAYiNsY1oOu4yvRUYgZP31oCNs7Mye/muiX
+rB30mF/ZSFJmXHSKEwegtmvc9yZ+HBWmV0GPySwjs+K0FEw1XexH9U8kliOvGexG
+fvqMXEDWe0oPYNbAOh8NHtRrr9nzARzDS0omlHqgKQKBgQDtHDL6dRZAkM2CBjwR
+ZgbMXGY+GJK6ElU9eqXK1vBud/AV286N1OVQ8wlazTHI/gEWzDtsenFqt2hhRX13
+sMIW2qmmtgOb4PdZdqe3+CMa97BoEziedbgoWFFLp+4NiYZzSli/lSF+OTMzdMsU
+wMD1vb7lyytAs1a+MhvgHO4iBQKBgQC9N/MkWvUlgy2+Ak9hti2egRhggCBaGWqY
+UAadYiCHhctvSgOwixe25mrvEe4aVDjMKglm/y2cCPXjPOThjfsi9UuehscDWdde
+f4UnFb0KiNmDV7MsaxG6EgRwOaOLtTuJeKQRNQPQ9VcUEdZxt7snzaNHHSlprL9c
+PMueQBPv/wKBgBhd8IM6qynBd80n9N5Y3NP9nug8wD9tCOODiiw5QIYpvzuP1j9P
+JK3X/BsfwUEFkXkVTfoM70DnTkvIx2cYfCm7GPov9Fj9mo3QGtZWIs1vrOpVJ1lp
+gZ5rzRb4UAeGHZIVjt9JZSLCoBdmpkQgtvPKJycYZP6GL6DmJ2U1s+c9AoGBAIp5
+2c0va90qJV27HxEpXDV10Ls+yW5mz2Xsmwqu95N2zS0DA7Q99vr5oiSYAKLwJCj2
+Uq837M8Wl6zXscGIQNSSo+a+SAMhysXzmSTDefets1G16wCE0xJTgUAITrI9zfaL
+fbbCD6rrAfFEJKZQif1VNzsiEl6t99WvAG0uA+lNAoGBAORDn6JlCSXA5nramwuW
+DgI0mjk1lOomn0yTMhv5xkFlW6Mm8oi/AIPmn0aibCluF2WmYv8tCsM+/1lzGW1W
+YNgfvkBTzAIGuqSWFJkUPJYkIXq/YeA2B7m2RfKizu8Jh/u89p9zytpSggt4wEmv
+8vhtPgyKb+J7Ao4ALzITvLKG
+-----END PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert-pwd.p12 b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert-pwd.p12
new file mode 100644
index 0000000000..f6615d9a16
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert-pwd.p12
Binary files differ
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.p12 b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.p12
new file mode 100644
index 0000000000..a1ddb2f915
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.p12
Binary files differ
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.pem
new file mode 100644
index 0000000000..3c9ecf3a9f
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-cert.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDhDCCAwmgAwIBAgIUOWjP4WDFqjNgjpi9+3RyLHVgVDowCgYIKoZIzj0EAwQw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYjAeFw0yNTAyMjcxNTMwNDBaFw0yNjAyMjcx
+NTMwNDBaMCUxDDAKBgNVBAoMA1lEQjEVMBMGA1UEAwwMaG9zdC55ZGIubmV0MIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0Gj5UzadnbHRckVcZI8BEFK
+DZZTjdVS5KXWQ3l/oNVEW5Ay+cYDQJWeO4+rdhXqCEsLPz0c6LCmZG8mBlz0Clp5
+aPkPsXOrZW+/qnW5hHq3BMJbyBLXcJjUXNShvbO+wO/mXN+RuJktxRxq9ZFc5Dxo
+23qSD2rnEsCO7+swXqzOI76x+a+euOMrWGN0vMaQBlfdkzE9FFhHNP3kUorSli5d
+IjF6TS2agmQM7VyKuodl2x/f/FeobzDqrTH5bpA0d1zj9maZaKpAC7Gt1Wkl7e7/
+48t3OOYpM4yNbzWUktLuEnMhySsCWZGu3hF3zSNpKaqh2ANlSK/J7KOC6VWN+wID
+AQABo4IBKjCCASYwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBsAwHwYJYIZI
+AYb4QgENBBIWEFRlc3QgY2VydGlmaWNhdGUwHQYDVR0OBBYEFHmk/j4vLRcLHPYk
+KGkjx141v5o9MIGABgNVHSMEeTB3gBTJuFIcFPa5d1GB9zT88xTJdTiDm6FJpEcw
+RTEMMAoGA1UECgwDWURCMRkwFwYDVQQDDBBZREIgVGVzdCBSb290IENBMRowGAYJ
+KoZIhvcNAQkBFgt5ZGJAeWRiLnlkYoIUWA7CEw96x7d1uASDwX0Axqa1IgUwCwYD
+VR0PBAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAXBgNVHREE
+EDAOggxob3N0LnlkYi5uZXQwCgYIKoZIzj0EAwQDaQAwZgIxAKF9+r38ij1iOdMl
+be+cT4r26S4Fo8rfDMP1YlIJ9dvlJjI7lPPW72441et4QvJEogIxAPp3GldSZFvu
+WFVUGSK0l2UNct5Rm5fegGKwMaunk/ZQO293JBc/AOU31FtgxcOLrg==
+-----END CERTIFICATE-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key-pwd.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key-pwd.pem
new file mode 100644
index 0000000000..5b012f7b94
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key-pwd.pem
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIj9j7mZP26RoCAggA
+MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCtR2G2S3gQV05HBrC2K8zJBIIE
+0NHU/ROqMHHu84Td4a7FdymDQzem/fnioBuQadWLGvQMs/rbqGm8iUvMFis3M5Aq
+wdHKUqO6UMKL4W+HCJtF2C/qFYkeWeAGOcddcuKbFSeOqTiiRFREpcX+MrFG9AJr
+RVVmjCo3NHWBWw17plhP2fG/wUoHTnFbN9KgwV38mWF0ixe2InexQETFZu3bgIXT
+VTV6+yVC++pDxhKFGmbzWu+rN3TwMAubpLy18c49BdYjmWvKdATcJ200SH9Lif4I
+9FSs745hJV0qNrCjRoZCji+pc1UoW+MNoj2F+m8kJBb0i4+LhaAtEOCBtj+V4oQJ
+dI38DBaPKwQS6LqczHmM4td93MyA44ubHzkkT2tMTmC5iPDIcG9599Idl5i6F0ZB
+99789+pei0IPlV0Kl2Z72SxdhOadlIeE+syeO1Jon0aP3b93xiodlvo5glEHNkAJ
+vmsJXaf1DReaIuDG+l8wtsKpvHQgHeIIRuTN4frned9X6XxwzvzLXWpBoL4q7M8A
+HPuPPrLyCpCh2wTbp9ukdE5zVPfCExduZ6b5atk5Wgxt7P1HDNp/nwHRG7TJbge6
+Krbi6OPwyU7xY8LiOwkhec4AzNelPNgaYvF1VgQtA0XkKz75cMv1ikwF2/OVnpFI
+U3QIEAvZAzxGZkbdqsGeboabtQ8vmY96QKqKBs3OIhEYq0OreA7YnQzppcCFEW3X
+Nt9wA06rBZRumx2MbZQ5NzOgEYVSXBcapA/yYR8wxyLKK/LqRvZXTanw3PojbooU
+zGXGxqTyYXPimSOlfgK9aRTBFsj0TGGAIbXZehTj36cNKoV0o9V912yRb7VpJIZY
+bRGHAbYpvOHkjvC6pi5sRDACkyma2H53uEkvZGMOva0Xfq/iDszeUNHeFAauMDvR
+OjdfXcJaUeO+PwUNyFpVZnmsQzsfU6FvfNcpiak6kQb2h2OlTXV1LQzCOTbUst6N
+fVv78wY+HlF6BG+pR/yWZEJvPQ0ui51v4FY+xK0/I0+so2lk2Pj3xSq2S+eqC56X
+s4CAPYzMfIZZBUhpBXeVjnMR2REjvsixoF6fghJso70aIhmbWvXGSw2LA07WRrmo
+zWmhGIJ/egBSPU8hb4XG7ZiybyFgAleyUFl5laI3F94zOAixsSj6o2FoiAFeiKN9
+hZevO27Aino/Ep6+fAJAIG6oi5F/svchrLtn0Mdr0iTU/jKbY1F151Vr6incv81f
+KkWheXr+NO0VaS1Qnh64lWV2rSSwQunLSZNL4SlMWxMTZykfDcAbb6B2UuNRKGnp
+ryYLDfFfY2YFQjT+lG84hkYeEY5mKZ/FFO1Vu59YxHaJLMCvfAoxbwXAFUCeN4h+
+WY/gPLXJLXtqEtSg8RvCIjUmqfq72zpjuu/KxVbrEmXWLA8RE0X3UolZz+kWxpCA
+JETc9S5+iexMi0ns/Fe7y8thMxHMbg5aBSU4oq50lj+u5PdYyJI+EejrWobjl07T
+T5mJid9uEo4e7+HLqNz3ebMKpT87VLk/r4AoQwZ39P+4N2s7o/C6IBGpOqADHASA
+ieLWLoKt/O+o4vM0Kfpme3fHg2tzv9SeX0ECf6ZF0suq/Vs8xtWhuIMEhrM/tyFt
+pGLEHhTOuaRsoOy0mVeZYugqhx7v6mnzoYKWzoKMfrMS
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key.pem b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key.pem
new file mode 100644
index 0000000000..99909c5804
--- /dev/null
+++ b/ydb/public/lib/ydb_cli/common/ut/test_data/rsa-private-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCvQaPlTNp2dsdF
+yRVxkjwEQUoNllON1VLkpdZDeX+g1URbkDL5xgNAlZ47j6t2FeoISws/PRzosKZk
+byYGXPQKWnlo+Q+xc6tlb7+qdbmEercEwlvIEtdwmNRc1KG9s77A7+Zc35G4mS3F
+HGr1kVzkPGjbepIPaucSwI7v6zBerM4jvrH5r5644ytYY3S8xpAGV92TMT0UWEc0
+/eRSitKWLl0iMXpNLZqCZAztXIq6h2XbH9/8V6hvMOqtMflukDR3XOP2ZploqkAL
+sa3VaSXt7v/jy3c45ikzjI1vNZSS0u4ScyHJKwJZka7eEXfNI2kpqqHYA2VIr8ns
+o4LpVY37AgMBAAECggEAFN3Q4M1MuYwc3884UWRi5vLLGWELo7iBfR3NrAz8SC78
+S5aYqyqLWpY118ucU5v6WHBu72GcXowh05V0Ro+sssrg58G3v54RCyvJ+0a3BvIB
+dnyZpvGvWwWnS0I527dZ/+jQWcB4vizmyTA//+sFz1rt9Svb1iAZUR9CJYiwIqVn
+xyDYbL3TzLacSs5iKzcbW4i6OeLphgAYiNsY1oOu4yvRUYgZP31oCNs7Mye/muiX
+rB30mF/ZSFJmXHSKEwegtmvc9yZ+HBWmV0GPySwjs+K0FEw1XexH9U8kliOvGexG
+fvqMXEDWe0oPYNbAOh8NHtRrr9nzARzDS0omlHqgKQKBgQDtHDL6dRZAkM2CBjwR
+ZgbMXGY+GJK6ElU9eqXK1vBud/AV286N1OVQ8wlazTHI/gEWzDtsenFqt2hhRX13
+sMIW2qmmtgOb4PdZdqe3+CMa97BoEziedbgoWFFLp+4NiYZzSli/lSF+OTMzdMsU
+wMD1vb7lyytAs1a+MhvgHO4iBQKBgQC9N/MkWvUlgy2+Ak9hti2egRhggCBaGWqY
+UAadYiCHhctvSgOwixe25mrvEe4aVDjMKglm/y2cCPXjPOThjfsi9UuehscDWdde
+f4UnFb0KiNmDV7MsaxG6EgRwOaOLtTuJeKQRNQPQ9VcUEdZxt7snzaNHHSlprL9c
+PMueQBPv/wKBgBhd8IM6qynBd80n9N5Y3NP9nug8wD9tCOODiiw5QIYpvzuP1j9P
+JK3X/BsfwUEFkXkVTfoM70DnTkvIx2cYfCm7GPov9Fj9mo3QGtZWIs1vrOpVJ1lp
+gZ5rzRb4UAeGHZIVjt9JZSLCoBdmpkQgtvPKJycYZP6GL6DmJ2U1s+c9AoGBAIp5
+2c0va90qJV27HxEpXDV10Ls+yW5mz2Xsmwqu95N2zS0DA7Q99vr5oiSYAKLwJCj2
+Uq837M8Wl6zXscGIQNSSo+a+SAMhysXzmSTDefets1G16wCE0xJTgUAITrI9zfaL
+fbbCD6rrAfFEJKZQif1VNzsiEl6t99WvAG0uA+lNAoGBAORDn6JlCSXA5nramwuW
+DgI0mjk1lOomn0yTMhv5xkFlW6Mm8oi/AIPmn0aibCluF2WmYv8tCsM+/1lzGW1W
+YNgfvkBTzAIGuqSWFJkUPJYkIXq/YeA2B7m2RfKizu8Jh/u89p9zytpSggt4wEmv
+8vhtPgyKb+J7Ao4ALzITvLKG
+-----END PRIVATE KEY-----
diff --git a/ydb/public/lib/ydb_cli/common/ut/ya.make b/ydb/public/lib/ydb_cli/common/ut/ya.make
index a77a975080..f73e26a026 100644
--- a/ydb/public/lib/ydb_cli/common/ut/ya.make
+++ b/ydb/public/lib/ydb_cli/common/ut/ya.make
@@ -1,6 +1,7 @@
UNITTEST_FOR(ydb/public/lib/ydb_cli/common)
SRCS(
+ cert_format_converter_ut.cpp
normalize_path_ut.cpp
csv_parser_ut.cpp
pg_dump_parser_ut.cpp
diff --git a/ydb/public/lib/ydb_cli/common/ya.make b/ydb/public/lib/ydb_cli/common/ya.make
index c0d94cc3e4..d8067dc57c 100644
--- a/ydb/public/lib/ydb_cli/common/ya.make
+++ b/ydb/public/lib/ydb_cli/common/ya.make
@@ -2,6 +2,7 @@ LIBRARY(common)
SRCS(
aws.cpp
+ cert_format_converter.cpp
command.cpp
command_utils.cpp
common.cpp
@@ -36,6 +37,7 @@ SRCS(
PEERDIR(
contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3
+ contrib/libs/openssl
library/cpp/config
library/cpp/getopt
library/cpp/json/writer
diff --git a/ydb/public/sdk/cpp/client/ydb_driver/driver.h b/ydb/public/sdk/cpp/client/ydb_driver/driver.h
index fd8bb8c95a..0cef2d1db4 100644
--- a/ydb/public/sdk/cpp/client/ydb_driver/driver.h
+++ b/ydb/public/sdk/cpp/client/ydb_driver/driver.h
@@ -50,7 +50,7 @@ public:
//! default: 0
TDriverConfig& SetMaxClientQueueSize(size_t sz);
//! Enable Ssl.
- //! caCerts - The buffer containing the PEM encoding of the server root certificates.
+ //! caCerts - The buffer containing the PEM encoded root certificates for SSL/TLS connections.
//! If this parameter is empty, the default roots will be used.
TDriverConfig& UseSecureConnection(const TStringType& caCerts = TStringType());
TDriverConfig& UseClientCertificate(const TStringType& clientCert, const TStringType& clientPrivateKey);
diff --git a/ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h b/ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h
index 9aad0ac0a2..c934c422cb 100644
--- a/ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h
+++ b/ydb/public/sdk/cpp/include/ydb-cpp-sdk/client/driver/driver.h
@@ -47,7 +47,7 @@ public:
//! default: 0
TDriverConfig& SetMaxClientQueueSize(size_t sz);
//! Enable Ssl.
- //! caCerts - The buffer containing the PEM encoding of the server root certificates.
+ //! caCerts - The buffer containing the PEM encoded root certificates for SSL/TLS connections.
//! If this parameter is empty, the default roots will be used.
TDriverConfig& UseSecureConnection(const std::string& caCerts = std::string());
TDriverConfig& UseClientCertificate(const std::string& clientCert, const std::string& clientPrivateKey);