diff --git a/dap-sdk/core/include/dap_file_utils.h b/dap-sdk/core/include/dap_file_utils.h
index 3bddee20a53f5c6612d2cc302c269873da95d41c..cd5ba8bfcde9d03f599ccc35111f14cd05947392 100755
--- a/dap-sdk/core/include/dap_file_utils.h
+++ b/dap-sdk/core/include/dap_file_utils.h
@@ -126,6 +126,50 @@ dap_list_name_directories_t *dap_get_subs(const char *a_path_name);
  */
 bool dap_file_get_contents(const char *filename, char **contents, size_t *length);
 
+/*
+ * Creates a path from a series of elements using @separator as the
+ * separator between elements. At the boundary between two elements,
+ * any trailing occurrences of separator in the first element, or
+ * leading occurrences of separator in the second element are removed
+ * and exactly one copy of the separator is inserted.
+ *
+ * @separator: (type filename): a string used to separator the elements of the path.
+ * @first_element: (type filename): the first element in the path
+ * @...: remaining elements in path, terminated by %NULL
+ *
+ * Returns: (type filename) (transfer full): a newly-allocated string that
+ *     must be freed with DAP_DELETE().
+ */
+char* dap_build_path(const char *separator, const char *first_element, ...);
+
+/*
+ * Creates a filename from a series of elements using the correct
+ * separator for filenames.
+ *
+ * @first_element: (type filename): the first element in the path
+ * @...: remaining elements in path, terminated by %NULL
+ *
+ * Returns: (type filename) (transfer full): a newly-allocated string that must be freed with DAP_DELETE().
+ */
+char *dap_build_filename (const char *first_element, ...);
+
+/*
+ * Gets the canonical file name from @filename. All triple slashes are turned into
+ * single slashes, and all `..` and `.`s resolved against @relative_to.
+ *
+ * @filename: (type filename): the name of the file
+ * @relative_to: (type filename) (nullable): the relative directory, or %NULL to use the current working directory
+ *
+ * Returns: (type filename) (transfer full): a newly allocated string with the canonical file path
+ */
+char* dap_canonicalize_filename(const char *filename, const char *relative_to);
+
+/*
+ * Gets the current directory
+ * Returns: (type filename) (transfer full): the current directory
+ */
+char* dap_get_current_dir(void);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/dap-sdk/core/src/dap_file_utils.c b/dap-sdk/core/src/dap_file_utils.c
index c9e6d2776e77daa31416d2491970bbb63e86a79e..53219f8bfa048f4a2d3e2649658f90b0b93531d7 100755
--- a/dap-sdk/core/src/dap_file_utils.c
+++ b/dap-sdk/core/src/dap_file_utils.c
@@ -30,6 +30,7 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
+#include <limits.h>
 #if (OS_TARGET == OS_MACOS)
     #include <stdio.h>
 #else
@@ -37,6 +38,7 @@
 #endif
 #include <string.h>
 #include <sys/stat.h>
+#include <stdarg.h>
 
 #ifdef _WIN32
 #include <windows.h>
@@ -44,6 +46,7 @@
 #endif
 
 #include "dap_common.h"
+#include "dap_string.h"
 #include "dap_strfuncs.h"
 #include "dap_file_utils.h"
 
@@ -72,13 +75,13 @@ bool dap_valid_ascii_symbols(const char *a_string)
  * @a_file_path filename pathname
  * @return true, if file exists
  */
-bool dap_file_simple_test(const char * a_file_path)
+bool dap_file_test(const char * a_file_path)
 {
     if(!a_file_path)
         return false;
 #ifdef _WIN32
     int attr = GetFileAttributesA(a_file_path);
-    if(attr != -1)
+    if(attr != -1 && (attr & FILE_ATTRIBUTE_NORMAL))
         return true;
 #else
     struct stat st;
@@ -96,13 +99,13 @@ bool dap_file_simple_test(const char * a_file_path)
  * @a_file_path filename pathname
  * @return true, if file exists
  */
-bool dap_file_test(const char * a_file_path)
+bool dap_file_simple_test(const char * a_file_path)
 {
     if(!a_file_path)
         return false;
 #ifdef _WIN32
     int attr = GetFileAttributesA(a_file_path);
-    if(attr != -1 && (attr & FILE_ATTRIBUTE_NORMAL))
+    if(attr != -1)
         return true;
 #else
     struct stat st;
@@ -288,7 +291,7 @@ bool dap_path_is_absolute(const char *a_file_name)
         return true;
 
 #ifdef _WIN32
-    /* Recognize drive letter on native Windows */
+    // Recognize drive letter on native Windows
     if (dap_ascii_isalpha(a_file_name[0]) &&
             a_file_name[1] == ':' && DAP_IS_DIR_SEPARATOR (a_file_name[2]))
     return true;
@@ -297,6 +300,74 @@ bool dap_path_is_absolute(const char *a_file_name)
     return false;
 }
 
+/**
+ * dap_path_skip_root:
+ * @file_name: (type filename): a file name
+ *
+ * Returns a pointer into @file_name after the root component,
+ * i.e. after the "/" in UNIX or "C:\" under Windows. If @file_name
+ * is not an absolute path it returns %NULL.
+ *
+ * Returns: (type filename) (nullable): a pointer into @file_name after the
+ *     root component
+ */
+const char *dap_path_skip_root (const char *file_name)
+{
+    dap_return_val_if_fail(file_name != NULL, NULL);
+
+    // Skip \\server\share or //server/share
+    if(DAP_IS_DIR_SEPARATOR (file_name[0]) &&
+            DAP_IS_DIR_SEPARATOR(file_name[1]) &&
+            file_name[2] &&
+            !DAP_IS_DIR_SEPARATOR(file_name[2]))
+    {
+        char *p;
+        p = strchr(file_name + 2, DAP_DIR_SEPARATOR);
+
+#ifdef _WIN32
+      {
+        char *q;
+        q = strchr (file_name + 2, '/');
+        if (p == NULL || (q != NULL && q < p))
+        p = q;
+      }
+#endif
+
+        if(p && p > file_name + 2 && p[1])
+                {
+            file_name = p + 1;
+
+            while(file_name[0] && !DAP_IS_DIR_SEPARATOR(file_name[0]))
+                file_name++;
+
+            // Possibly skip a backslash after the share name
+            if(DAP_IS_DIR_SEPARATOR(file_name[0]))
+                file_name++;
+
+            return (char*) file_name;
+        }
+    }
+
+    // Skip initial slashes
+    if(DAP_IS_DIR_SEPARATOR(file_name[0]))
+            {
+        while(DAP_IS_DIR_SEPARATOR(file_name[0]))
+            file_name++;
+        return (char*) file_name;
+    }
+
+#ifdef _WIN32
+  /* Skip X:\ */
+  if (dap_ascii_isalpha (file_name[0]) &&
+      file_name[1] == ':' &&
+      DAP_IS_DIR_SEPARATOR (file_name[2]))
+    return (char *)file_name + 3;
+#endif
+
+    return NULL;
+}
+
+
 /**
  * dap_path_get_dirname:
  * @a_file_name: the name of the file
@@ -666,3 +737,564 @@ bool dap_file_get_contents(const char *filename, char **contents, size_t *length
 #endif
 }
 
+
+
+
+static char* dap_build_path_va(const char *separator, const char *first_element, va_list *args, char **str_array)
+{
+    dap_string_t *result;
+    int separator_len = dap_strlen(separator);
+    bool is_first = TRUE;
+    bool have_leading = FALSE;
+    const char *single_element = NULL;
+    const char *next_element;
+    const char *last_trailing = NULL;
+    int i = 0;
+
+    result = dap_string_new(NULL);
+
+    if(str_array)
+        next_element = str_array[i++];
+    else
+        next_element = first_element;
+
+    while(TRUE)
+    {
+        const char *element;
+        const char *start;
+        const char *end;
+
+        if(next_element)
+        {
+            element = next_element;
+            if(str_array)
+                next_element = str_array[i++];
+            else
+                next_element = va_arg(*args, char*);
+        }
+        else
+            break;
+
+        // Ignore empty elements
+        if(!*element)
+            continue;
+
+        start = element;
+
+        if(separator_len)
+        {
+            while(dap_strncmp(start, separator, separator_len) == 0)
+                start += separator_len;
+        }
+
+        end = start + dap_strlen(start);
+
+        if(separator_len)
+        {
+            while(end >= start + separator_len &&
+                    dap_strncmp(end - separator_len, separator, separator_len) == 0)
+                end -= separator_len;
+
+            last_trailing = end;
+            while(last_trailing >= element + separator_len &&
+                    dap_strncmp(last_trailing - separator_len, separator, separator_len) == 0)
+                last_trailing -= separator_len;
+
+            if(!have_leading)
+            {
+                // If the leading and trailing separator strings are in the same element and overlap, the result is exactly that element
+                if(last_trailing <= start)
+                    single_element = element;
+
+                dap_string_append_len(result, element, start - element);
+                have_leading = TRUE;
+            }
+            else
+                single_element = NULL;
+        }
+
+        if(end == start)
+            continue;
+
+        if(!is_first)
+            dap_string_append(result, separator);
+
+        dap_string_append_len(result, start, end - start);
+        is_first = FALSE;
+    }
+
+    if(single_element)
+    {
+        dap_string_free(result, TRUE);
+        return dap_strdup(single_element);
+    }
+    else
+    {
+        if(last_trailing)
+            dap_string_append(result, last_trailing);
+
+        return dap_string_free(result, FALSE);
+    }
+}
+
+/**
+ * dap_build_pathv:
+ * @separator: a string used to separator the elements of the path.
+ * @args: (array zero-terminated=1) (element-type filename): %NULL-terminated
+ *     array of strings containing the path elements.
+ *
+ * Behaves exactly like g_build_path(), but takes the path elements
+ * as a string array, instead of varargs. This function is mainly
+ * meant for language bindings.
+ *
+ * Returns: (type filename) (transfer full): a newly-allocated string that
+ *     must be freed with DAP_DELETE().
+ *
+ */
+char* dap_build_pathv(const char *separator, char **args)
+{
+    if(!args)
+        return NULL;
+
+    return dap_build_path_va(separator, NULL, NULL, args);
+}
+
+/**
+ * dap_build_path:
+ * @separator: (type filename): a string used to separator the elements of the path.
+ * @first_element: (type filename): the first element in the path
+ * @...: remaining elements in path, terminated by %NULL
+ *
+ * Creates a path from a series of elements using @separator as the
+ * separator between elements. At the boundary between two elements,
+ * any trailing occurrences of separator in the first element, or
+ * leading occurrences of separator in the second element are removed
+ * and exactly one copy of the separator is inserted.
+ *
+ * Empty elements are ignored.
+ *
+ * The number of leading copies of the separator on the result is
+ * the same as the number of leading copies of the separator on
+ * the first non-empty element.
+ *
+ * The number of trailing copies of the separator on the result is
+ * the same as the number of trailing copies of the separator on
+ * the last non-empty element. (Determination of the number of
+ * trailing copies is done without stripping leading copies, so
+ * if the separator is `ABA`, then `ABABA` has 1 trailing copy.)
+ *
+ * However, if there is only a single non-empty element, and there
+ * are no characters in that element not part of the leading or
+ * trailing separators, then the result is exactly the original value
+ * of that element.
+ *
+ * Other than for determination of the number of leading and trailing
+ * copies of the separator, elements consisting only of copies
+ * of the separator are ignored.
+ *
+ * Returns: (type filename) (transfer full): a newly-allocated string that
+ *     must be freed with DAP_DELETE().
+ **/
+char* dap_build_path(const char *separator, const char *first_element, ...)
+{
+    char *str;
+    va_list args;
+
+    dap_return_val_if_fail(separator != NULL, NULL);
+
+    va_start(args, first_element);
+    str = dap_build_path_va(separator, first_element, &args, NULL);
+    va_end(args);
+
+    return str;
+}
+
+#ifdef _WIN32
+
+static char* dap_build_pathname_va(const char *first_element, va_list *args, char **str_array)
+{
+    /* Code copied from g_build_pathv(), and modified to use two
+     * alternative single-character separators.
+     */
+    dap_string_t *result;
+    bool is_first = TRUE;
+    bool have_leading = FALSE;
+    const char *single_element = NULL;
+    const char *next_element;
+    const char *last_trailing = NULL;
+    char current_separator = '\\';
+    int i = 0;
+
+    result = dap_string_new(NULL);
+
+    if(str_array)
+        next_element = str_array[i++];
+    else
+        next_element = first_element;
+
+    while(TRUE)
+    {
+        const char *element;
+        const char *start;
+        const char *end;
+
+        if(next_element)
+        {
+            element = next_element;
+            if(str_array)
+                next_element = str_array[i++];
+            else
+                next_element = va_arg(*args, char*);
+        }
+        else
+            break;
+
+        /* Ignore empty elements */
+        if(!*element)
+            continue;
+
+        start = element;
+
+        if(TRUE)
+        {
+            while(start &&
+                    (*start == '\\' || *start == '/'))
+            {
+                current_separator = *start;
+                start++;
+            }
+        }
+
+        end = start + strlen(start);
+
+        if(TRUE)
+        {
+            while(end >= start + 1 &&
+                    (end[-1] == '\\' || end[-1] == '/'))
+            {
+                current_separator = end[-1];
+                end--;
+            }
+
+            last_trailing = end;
+            while(last_trailing >= element + 1 &&
+                    (last_trailing[-1] == '\\' || last_trailing[-1] == '/'))
+                last_trailing--;
+
+            if(!have_leading)
+            {
+                // If the leading and trailing separator strings are in the same element and overlap, the result is exactly that element
+                if(last_trailing <= start)
+                    single_element = element;
+
+                dap_string_append_len(result, element, start - element);
+                have_leading = TRUE;
+            }
+            else
+                single_element = NULL;
+        }
+
+        if(end == start)
+            continue;
+
+        if(!is_first)
+            dap_string_append_len(result, &current_separator, 1);
+
+        dap_string_append_len(result, start, end - start);
+        is_first = FALSE;
+    }
+
+    if(single_element)
+    {
+        dap_string_free(result, TRUE);
+        return dap_strdup(single_element);
+    }
+    else
+    {
+        if(last_trailing)
+            dap_string_append(result, last_trailing);
+
+        return dap_string_free(result, FALSE);
+    }
+}
+
+#endif
+
+static char* dap_build_filename_va(const char *first_argument, va_list *args, char **str_array)
+{
+    char *str;
+
+#ifndef _WIN32
+    str = dap_build_path_va(DAP_DIR_SEPARATOR_S, first_argument, args, str_array);
+#else
+    str = dap_build_pathname_va(first_argument, args, str_array);
+#endif
+
+    return str;
+}
+
+/**
+ * dap_build_filename:
+ * @first_element: (type filename): the first element in the path
+ * @...: remaining elements in path, terminated by %NULL
+ *
+ * Creates a filename from a series of elements using the correct
+ * separator for filenames.
+ *
+ * On Unix, this function behaves identically to `g_build_path
+ * (G_DIR_SEPARATOR_S, first_element, ....)`.
+ *
+ * On Windows, it takes into account that either the backslash
+ * (`\` or slash (`/`) can be used as separator in filenames, but
+ * otherwise behaves as on UNIX. When file pathname separators need
+ * to be inserted, the one that last previously occurred in the
+ * parameters (reading from left to right) is used.
+ *
+ * No attempt is made to force the resulting filename to be an absolute
+ * path. If the first element is a relative path, the result will
+ * be a relative path.
+ *
+ * Returns: (type filename) (transfer full): a newly-allocated string that
+ *     must be freed with DAP_DELETE().
+ **/
+char* dap_build_filename(const char *first_element, ...)
+{
+    char *str;
+    va_list args;
+
+    va_start(args, first_element);
+    str = dap_build_filename_va(first_element, &args, NULL);
+    va_end(args);
+
+    return str;
+}
+
+/**
+ * dap_canonicalize_filename:
+ * @filename: (type filename): the name of the file
+ * @relative_to: (type filename) (nullable): the relative directory, or %NULL
+ * to use the current working directory
+ *
+ * Gets the canonical file name from @filename. All triple slashes are turned into
+ * single slashes, and all `..` and `.`s resolved against @relative_to.
+ *
+ * Symlinks are not followed, and the returned path is guaranteed to be absolute.
+ *
+ * If @filename is an absolute path, @relative_to is ignored. Otherwise,
+ * @relative_to will be prepended to @filename to make it absolute. @relative_to
+ * must be an absolute path, or %NULL. If @relative_to is %NULL, it'll fallback
+ * to g_get_current_dir().
+ *
+ * This function never fails, and will canonicalize file paths even if they don't
+ * exist.
+ *
+ * No file system I/O is done.
+ *
+ * Returns: (type filename) (transfer full): a newly allocated string with the
+ * canonical file path
+ */
+char* dap_canonicalize_filename(const char *filename, const char *relative_to)
+{
+    char *canon, *input, *output, *after_root, *output_start;
+
+    dap_return_val_if_fail(relative_to == NULL || dap_path_is_absolute (relative_to), NULL);
+
+    if(!dap_path_is_absolute(filename)) {
+        char *cwd_allocated = NULL;
+        const char *cwd;
+        if(relative_to != NULL)
+            cwd = relative_to;
+        else
+            cwd = cwd_allocated = dap_get_current_dir();
+
+        canon = dap_build_filename(cwd, filename, NULL);
+        DAP_DELETE(cwd_allocated);
+    }
+    else
+    {
+        canon = dap_strdup(filename);
+    }
+
+    after_root = (char*) dap_path_skip_root(canon);
+
+    if(after_root == NULL)
+    {
+        // This shouldn't really happen
+        DAP_DELETE(canon);
+        return dap_build_filename(DAP_DIR_SEPARATOR_S, filename, NULL);
+    }
+
+    // Find the first dir separator and use the canonical dir separator.
+    for(output = after_root - 1;
+            (output >= canon) && DAP_IS_DIR_SEPARATOR(*output);
+            output--)
+        *output = DAP_DIR_SEPARATOR;
+
+    /* 1 to re-increment after the final decrement above (so that output >= canon),
+     * and 1 to skip the first `/`. There might not be a first `/` if
+     * the @canon is a Windows `//server/share` style path with no
+     * trailing directories. @after_root will be '\0' in that case. */
+    output++;
+    if(*output == DAP_DIR_SEPARATOR)
+        output++;
+
+    /* POSIX allows double slashes at the start to mean something special
+     * (as does windows too). So, "//" != "/", but more than two slashes
+     * is treated as "/".
+     */
+    if(after_root - output == 1)
+        output++;
+
+    input = after_root;
+    output_start = output;
+    while(*input)
+    {
+        /* input points to the next non-separator to be processed. */
+        /* output points to the next location to write to. */
+        assert(input > canon && DAP_IS_DIR_SEPARATOR(input[-1]));
+        assert(output > canon && DAP_IS_DIR_SEPARATOR(output[-1]));
+        assert(input >= output);
+
+        /* Ignore repeated dir separators. */
+        while(DAP_IS_DIR_SEPARATOR(input[0]))
+            input++;
+
+        /* Ignore single dot directory components. */
+        if(input[0] == '.' && (input[1] == 0 || DAP_IS_DIR_SEPARATOR(input[1])))
+                {
+            if(input[1] == 0)
+                break;
+            input += 2;
+        }
+        /* Remove double-dot directory components along with the preceding
+         * path component. */
+        else if(input[0] == '.' && input[1] == '.' &&
+                (input[2] == 0 || DAP_IS_DIR_SEPARATOR(input[2])))
+                {
+            if(output > output_start)
+                    {
+                do
+                {
+                    output--;
+                }
+                while(!DAP_IS_DIR_SEPARATOR(output[-1]) && output > output_start);
+            }
+            if(input[2] == 0)
+                break;
+            input += 3;
+        }
+        /* Copy the input to the output until the next separator,
+         * while converting it to canonical separator */
+        else
+        {
+            while(*input && !DAP_IS_DIR_SEPARATOR(*input))
+                *output++ = *input++;
+            if(input[0] == 0)
+                break;
+            input++;
+            *output++ = DAP_DIR_SEPARATOR;
+        }
+    }
+
+    /* Remove a potentially trailing dir separator */
+    if(output > output_start && DAP_IS_DIR_SEPARATOR(output[-1]))
+        output--;
+
+    *output = '\0';
+
+    return canon;
+}
+
+
+#if defined(MAXPATHLEN)
+#define DAP_PATH_LENGTH MAXPATHLEN
+#elif defined(PATH_MAX)
+#define DAP_PATH_LENGTH PATH_MAX
+#elif defined(_PC_PATH_MAX)
+#define DAP_PATH_LENGTH sysconf(_PC_PATH_MAX)
+#else
+#define DAP_PATH_LENGTH 2048
+#endif
+
+/**
+ * dap_get_current_dir:
+ *
+ * Gets the current directory.
+ *
+ * The returned string should be freed when no longer needed.
+ * The encoding of the returned string is system defined.
+ * On Windows, it is always UTF-8.
+ *
+ * Since GLib 2.40, this function will return the value of the "PWD"
+ * environment variable if it is set and it happens to be the same as
+ * the current directory.  This can make a difference in the case that
+ * the current directory is the target of a symbolic link.
+ *
+ * Returns: (type filename) (transfer full): the current directory
+ */
+char* dap_get_current_dir(void)
+{
+#ifdef _WIN32
+
+  gchar *dir = NULL;
+  wchar_t dummy[2], *wdir;
+  DWORD len;
+
+  len = GetCurrentDirectoryW (2, dummy);
+  wdir = g_new (wchar_t, len);
+
+  if (GetCurrentDirectoryW (len, wdir) == len - 1)
+    dir = g_utf16_to_utf8 (wdir, -1, NULL, NULL, NULL);
+
+  g_free (wdir);
+
+  if (dir == NULL)
+    dir = g_strdup ("\\");
+
+  return dir;
+
+#else
+    const char *pwd;
+    char *buffer = NULL;
+    char *dir = NULL;
+    static ulong max_len = 0;
+    struct stat pwdbuf, dotbuf;
+
+    pwd = getenv("PWD");
+    if(pwd != NULL &&
+            stat(".", &dotbuf) == 0 && stat(pwd, &pwdbuf) == 0 &&
+            dotbuf.st_dev == pwdbuf.st_dev && dotbuf.st_ino == pwdbuf.st_ino)
+        return dap_strdup(pwd);
+
+    if(max_len == 0)
+        max_len = (DAP_PATH_LENGTH == -1) ? 2048 : DAP_PATH_LENGTH;
+
+    while(max_len < ULONG_MAX / 2)
+    {
+        DAP_DELETE(buffer);
+        buffer = DAP_NEW_SIZE(char, max_len + 1);
+        *buffer = 0;
+        dir = getcwd(buffer, max_len);
+
+        if(dir || errno != ERANGE)
+            break;
+
+        max_len *= 2;
+    }
+
+    if(!dir || !*buffer)
+            {
+        /* hm, should we g_error() out here?
+         * this can happen if e.g. "./" has mode \0000
+         */
+        buffer[0] = DAP_DIR_SEPARATOR;
+        buffer[1] = 0;
+    }
+
+    dir = dap_strdup(buffer);
+    DAP_DELETE(buffer);
+
+    return dir;
+
+#endif
+}
diff --git a/modules/net/dap_chain_net.c b/modules/net/dap_chain_net.c
index 439b41330722a621b574fec9725cde84cf90544c..3f254cca9385b37bbe5ba23cdce7c11ad79fdfa9 100644
--- a/modules/net/dap_chain_net.c
+++ b/modules/net/dap_chain_net.c
@@ -3444,21 +3444,35 @@ static uint8_t *dap_chain_net_set_acl(dap_chain_hash_fast_t *a_pkey_hash)
  * @brief dap_cert_chain_file_save
  * @param datum
  */
-int dap_cert_chain_file_save(dap_chain_datum_t * datum, char * net_name)
+int dap_cert_chain_file_save(dap_chain_datum_t *datum, char *net_name)
 {
-    const char * s_system_chain_ca_dir = dap_config_get_item_str(g_config, "resources", "chain_ca_folder");
-
-    dap_cert_t * cert = dap_cert_mem_load(datum->data, datum->header.data_size);
-    const char * cert_name = cert->name;
-
-    size_t cert_path_length = strlen(net_name)+strlen(cert_name)+9+strlen(s_system_chain_ca_dir);
-    char *cert_path = DAP_NEW_Z_SIZE(char,cert_path_length);
-
-    snprintf(cert_path,cert_path_length,"%s/%s/%s.dcert",s_system_chain_ca_dir,net_name,cert_name);
-
+    const char *s_system_chain_ca_dir = dap_config_get_item_str(g_config, "resources", "chain_ca_folder");
+    if(dap_strlen(s_system_chain_ca_dir) == 0) {
+        log_it(L_ERROR, "Not found 'chain_ca_folder' in .cfg file");
+        return -1;
+    }
+    dap_cert_t *cert = dap_cert_mem_load(datum->data, datum->header.data_size);
+    if(!cert) {
+        log_it(L_ERROR, "Can't load cert, size: %d", datum->header.data_size);
+        return -1;
+    }
+    const char *cert_name = cert->name;
+    size_t cert_path_length = dap_strlen(net_name) + dap_strlen(cert_name) + 9 + dap_strlen(s_system_chain_ca_dir);
+    char *cert_path = DAP_NEW_Z_SIZE(char, cert_path_length);
+    snprintf(cert_path, cert_path_length, "%s/%s/%s.dcert", s_system_chain_ca_dir, net_name, cert_name);
+    // In cert_path resolve all `..` and `.`s
+    char *cert_path_c = dap_canonicalize_filename(cert_path, NULL);
+    DAP_DELETE(cert_path);
+    // Protect the ca folder from using "/.." in cert_name
+    if(dap_strncmp(s_system_chain_ca_dir, cert_path_c, dap_strlen(s_system_chain_ca_dir))) {
+        log_it(L_ERROR, "Cert path '%s' is not in ca dir: %s", cert_path_c, s_system_chain_ca_dir);
+        return -1;
+    }
+    int l_ret = dap_cert_file_save(cert, cert_path_c);
+    DAP_DELETE(cert_path_c);
 //  if ( access( l_cert_path, F_OK ) != -1 ) {
 //      log_it (L_ERROR, "File %s is already exists.", l_cert_path);
 //      return -1;
 //  } else
-    return dap_cert_file_save(cert, cert_path);
+    return l_ret;
 }