diff --git a/doc/dhtnode.1 b/doc/dhtnode.1
index 69e18eb4184b1ae4e22cdc36c57d3f816ec20357..ef456e54ee18630adb38af8cb1df16642451a2bc 100644
--- a/doc/dhtnode.1
+++ b/doc/dhtnode.1
@@ -7,7 +7,7 @@
 .SH SYNOPSIS
 .B dhtnode [-h]
 
-.B dhtnode [-v [-l \fIlogfile\fP]] [-i] [-d] [-n \fInetwork_id\fP] [-p \fIlocal_port\fP] [-b \fIbootstrap_host\fP[:\fIport\fP]]
+.B dhtnode [-v [-l \fIlogfile\fP]] [-i [--save-identity \fIfile\fP]] [-d] [-n \fInetwork_id\fP] [-p \fIlocal_port\fP] [-b \fIbootstrap_host\fP[:\fIport\fP]] [--certificate \fIfile\fP] [--privkey \fIfile\fP] [--privkey-password \fIpassword\fP]
 
 .SH DESCRIPTION
 
@@ -60,6 +60,22 @@ Write log to syslog instead of stdout
 \fB-i\fP
 Generate cryptographic identity for the node.
 
+.TP
+\fB--save-identity\fP \fIfile\fP
+Save generated identity (certificate and private key) to given file prefix.
+
+.TP
+\fB--certificate\fP \fIfile\fP
+Load identity certificate from given file.
+
+.TP
+\fB--privkey\fP \fIfile\fP
+Load identity private key from given file.
+
+.TP
+\fB--privkey-password\fP \fIpassword\fP
+Password to use for private key encryption or decryption (optional).
+
 .TP
 \fB-d\fP
 Run the program in daemon mode (will fork in the background).
diff --git a/include/opendht/crypto.h b/include/opendht/crypto.h
index 6782ac7b1e8632c1a8e7ba2dffcb15feeb387937..6f66db26ca870af24f4a0ea05f62fc0cd39d26d1 100644
--- a/include/opendht/crypto.h
+++ b/include/opendht/crypto.h
@@ -654,6 +654,7 @@ OPENDHT_PUBLIC Identity generateIdentity(const std::string& name = "dhtnode", co
 OPENDHT_PUBLIC Identity generateEcIdentity(const std::string& name, const Identity& ca, bool is_ca);
 OPENDHT_PUBLIC Identity generateEcIdentity(const std::string& name = "dhtnode", const Identity& ca = {});
 
+OPENDHT_PUBLIC void saveIdentity(const Identity& id, const std::string& path, const std::string& privkey_password = {});
 
 /**
  * Performs SHA512, SHA256 or SHA1, depending on hash_length.
diff --git a/src/crypto.cpp b/src/crypto.cpp
index 62ef4f5042ad129ed2afdf5fd1d6a3d9f06a0a40..5e0e92e9f17567ad55b890abfaf060dafcbd2141 100644
--- a/src/crypto.cpp
+++ b/src/crypto.cpp
@@ -31,6 +31,7 @@ extern "C" {
 
 #include <random>
 #include <sstream>
+#include <fstream>
 #include <stdexcept>
 #include <cassert>
 
@@ -1043,6 +1044,21 @@ generateEcIdentity(const std::string& name, const Identity& ca) {
     return generateEcIdentity(name, ca, !ca.first || !ca.second);
 }
 
+void
+saveIdentity(const Identity& id, const std::string& path, const std::string& privkey_password)
+{
+    {
+        auto ca_key_data = id.first->serialize(privkey_password);
+        std::ofstream key_file(path + ".pem");
+        key_file.write((char*)ca_key_data.data(), ca_key_data.size());
+    }
+    {
+        auto ca_key_data = id.second->getPacked();
+        std::ofstream crt_file(path + ".crt");
+        crt_file.write((char*)ca_key_data.data(), ca_key_data.size());
+    }
+}
+
 void
 setValidityPeriod(gnutls_x509_crt_t cert, int64_t validity)
 {
diff --git a/tools/dhtnode.cpp b/tools/dhtnode.cpp
index 14570e1a2042c7ed5639d055ab22df6ccc23eef3..db87a5e20e7cb5724d6eda57cc72acfd11782455 100644
--- a/tools/dhtnode.cpp
+++ b/tools/dhtnode.cpp
@@ -56,7 +56,7 @@ void print_node_info(const std::shared_ptr<DhtRunner>& node, const dht_params& p
         std::cout << "port " << port4 << std::endl;
     else
         std::cout << "IPv4 port " << port4 << ", IPv6 port " << port6 << std::endl;
-    if (params.generate_identity)
+    if (params.id.first)
         std::cout << "Public key ID " << node->getId() << std::endl;
 }
 
@@ -395,7 +395,7 @@ void cmd_loop(std::shared_ptr<DhtRunner>& node, dht_params& params
             node->cancelPut(id, std::stoul(rem, nullptr, 16));
         }
         else if (op == "s") {
-            if (not params.generate_identity) {
+            if (not params.id.first) {
                 print_id_req();
                 continue;
             }
@@ -410,7 +410,7 @@ void cmd_loop(std::shared_ptr<DhtRunner>& node, dht_params& params
             });
         }
         else if (op == "e") {
-            if (not params.generate_identity) {
+            if (not params.id.first) {
                 print_id_req();
                 continue;
             }
@@ -510,17 +510,20 @@ main(int argc, char **argv)
     auto node = std::make_shared<DhtRunner>();
 
     try {
-        dht::crypto::Identity crt {};
-        if (params.generate_identity) {
+        if (not params.id.first and params.generate_identity) {
             auto ca_tmp = dht::crypto::generateEcIdentity("DHT Node CA");
-            crt = dht::crypto::generateIdentity("DHT Node", ca_tmp);
+            params.id = dht::crypto::generateIdentity("DHT Node", ca_tmp);
+            if (not params.save_identity.empty()) {
+                dht::crypto::saveIdentity(ca_tmp, params.save_identity + "_ca", params.privkey_pwd);
+                dht::crypto::saveIdentity(params.id, params.save_identity, params.privkey_pwd);
+            }
         }
 
         dht::DhtRunner::Config config {};
         config.dht_config.node_config.network = params.network;
         config.dht_config.node_config.maintain_storage = false;
         config.dht_config.node_config.persist_path = params.persist_path;
-        config.dht_config.id = crt;
+        config.dht_config.id = params.id;
         config.threaded = true;
         config.proxy_server = params.proxyclient;
         config.push_node_id = "dhtnode";
diff --git a/tools/tools_common.h b/tools/tools_common.h
index de16bd3946803a924ae167327b9bf63c4a4c801e..d0cb757be6d1334082feaff0f2f8c2a90bde4c03 100644
--- a/tools/tools_common.h
+++ b/tools/tools_common.h
@@ -23,6 +23,7 @@
 
 #include <opendht.h>
 #include <opendht/log.h>
+#include <opendht/crypto.h>
 #ifndef WIN32_NATIVE
 #include <getopt.h>
 #include <readline/readline.h>
@@ -87,53 +88,79 @@ bool isInfoHash(const dht::InfoHash& h) {
     return true;
 }
 
+std::vector<uint8_t>
+loadFile(const std::string& path)
+{
+    std::vector<uint8_t> buffer;
+    std::ifstream file(path, std::ios::binary);
+    if (!file)
+        throw std::runtime_error("Can't read file: "+path);
+    file.seekg(0, std::ios::end);
+    auto size = file.tellg();
+    if (size > std::numeric_limits<unsigned>::max())
+        throw std::runtime_error("File is too big: "+path);
+    buffer.resize(size);
+    file.seekg(0, std::ios::beg);
+    if (!file.read((char*)buffer.data(), size))
+        throw std::runtime_error("Can't load file: "+path);
+    return buffer;
+}
+
 static const constexpr in_port_t DHT_DEFAULT_PORT = 4222;
 
 struct dht_params {
     bool help {false}; // print help and exit
     bool version {false};
-    bool log {false};
-    std::string logfile {};
-    bool syslog {false};
-    in_port_t port {0};
-    dht::NetId network {0};
     bool generate_identity {false};
     bool daemonize {false};
     bool service {false};
     bool peer_discovery {false};
+    bool log {false};
+    bool syslog {false};
+    std::string logfile {};
     std::pair<std::string, std::string> bootstrap {};
+    dht::NetId network {0};
+    in_port_t port {0};
     in_port_t proxyserver {0};
     std::string proxyclient {};
     std::string pushserver {};
     std::string devicekey {};
     std::string persist_path {};
+    dht::crypto::Identity id {};
+    std::string privkey_pwd {};
+    std::string save_identity {};
 };
 
 static const constexpr struct option long_options[] = {
-   {"help",             no_argument      , nullptr, 'h'},
-   {"port",             required_argument, nullptr, 'p'},
-   {"net",              required_argument, nullptr, 'n'},
-   {"bootstrap",        required_argument, nullptr, 'b'},
-   {"identity",         no_argument      , nullptr, 'i'},
-   {"verbose",          no_argument      , nullptr, 'v'},
-   {"daemonize",        no_argument      , nullptr, 'd'},
-   {"service",          no_argument      , nullptr, 's'},
-   {"peer-discovery",   no_argument      , nullptr, 'D'},
-   {"persist",          required_argument, nullptr, 'f'},
-   {"logfile",          required_argument, nullptr, 'l'},
-   {"syslog",           no_argument      , nullptr, 'L'},
-   {"proxyserver",      required_argument, nullptr, 'S'},
-   {"proxyclient",      required_argument, nullptr, 'C'},
-   {"pushserver",       required_argument, nullptr, 'y'},
-   {"devicekey",        required_argument, nullptr, 'z'},
-   {"version",          no_argument      , nullptr, 'V'},
-   {nullptr,            0                , nullptr,  0}
+    {"help",             no_argument      , nullptr, 'h'},
+    {"port",             required_argument, nullptr, 'p'},
+    {"net",              required_argument, nullptr, 'n'},
+    {"bootstrap",        required_argument, nullptr, 'b'},
+    {"identity",         no_argument      , nullptr, 'i'},
+    {"save-identity",    required_argument, nullptr, 'I'},
+    {"certificate",      required_argument, nullptr, 'c'},
+    {"privkey",          required_argument, nullptr, 'k'},
+    {"privkey-password", required_argument, nullptr, 'm'},
+    {"verbose",          no_argument      , nullptr, 'v'},
+    {"daemonize",        no_argument      , nullptr, 'd'},
+    {"service",          no_argument      , nullptr, 's'},
+    {"peer-discovery",   no_argument      , nullptr, 'D'},
+    {"persist",          required_argument, nullptr, 'f'},
+    {"logfile",          required_argument, nullptr, 'l'},
+    {"syslog",           no_argument      , nullptr, 'L'},
+    {"proxyserver",      required_argument, nullptr, 'S'},
+    {"proxyclient",      required_argument, nullptr, 'C'},
+    {"pushserver",       required_argument, nullptr, 'y'},
+    {"devicekey",        required_argument, nullptr, 'z'},
+    {"version",          no_argument      , nullptr, 'V'},
+    {nullptr,            0                , nullptr,  0}
 };
 
 dht_params
 parseArgs(int argc, char **argv) {
     dht_params params;
     int opt;
+    std::string privkey;
     while ((opt = getopt_long(argc, argv, "hidsvDp:n:b:f:l:", long_options, nullptr)) != -1) {
         switch (opt) {
         case 'p': {
@@ -201,10 +228,36 @@ parseArgs(int argc, char **argv) {
         case 's':
             params.service = true;
             break;
+        case 'c': {
+            try {
+                params.id.second = std::make_shared<dht::crypto::Certificate>(loadFile(optarg));
+            } catch (const std::exception& e) {
+                throw std::runtime_error(std::string("Error loading certificate: ") + e.what());
+            }
+            break;
+        }
+        case 'k':
+            privkey = optarg;
+            break;
+        case 'm':
+            params.privkey_pwd = optarg;
+            break;
+        case 'I':
+            params.save_identity = optarg;
+            break;
         default:
             break;
         }
     }
+    if (not privkey.empty()) {
+        try {
+            params.id.first = std::make_shared<dht::crypto::PrivateKey>(loadFile(privkey), params.privkey_pwd);
+        } catch (const std::exception& e) {
+            throw std::runtime_error(std::string("Error loading private key: ") + e.what());
+        }
+    }
+    if (params.save_identity.empty())
+        params.privkey_pwd.clear();
     return params;
 }