From 31728cb41c74ce72696f532e7bf4162e63b246da Mon Sep 17 00:00:00 2001 From: Georges Oates_Larsen Date: Tue, 26 Sep 2023 16:09:30 -0700 Subject: [PATCH] net: tls_credetials: Add TLS Credentials shell Adds a shell interface for TLS Credentials, allowing management of credentials via the Zephyr shell Signed-off-by: Georges Oates_Larsen --- doc/connectivity/networking/api/sockets.rst | 2 + .../networking/api/system_mgmt.rst | 1 + .../networking/api/tls_credentials_shell.rst | 259 ++++++ subsys/net/lib/tls_credentials/CMakeLists.txt | 3 + subsys/net/lib/tls_credentials/Kconfig | 2 + subsys/net/lib/tls_credentials/Kconfig.shell | 35 + .../tls_credentials/tls_credentials_shell.c | 811 ++++++++++++++++++ 7 files changed, 1113 insertions(+) create mode 100644 doc/connectivity/networking/api/tls_credentials_shell.rst create mode 100644 subsys/net/lib/tls_credentials/Kconfig.shell create mode 100644 subsys/net/lib/tls_credentials/tls_credentials_shell.c diff --git a/doc/connectivity/networking/api/sockets.rst b/doc/connectivity/networking/api/sockets.rst index 3f632e3bea93e3a..720bebeb820061e 100644 --- a/doc/connectivity/networking/api/sockets.rst +++ b/doc/connectivity/networking/api/sockets.rst @@ -73,6 +73,8 @@ To enable secure sockets, set the :kconfig:option:`CONFIG_NET_SOCKETS_SOCKOPT_TL option. To enable DTLS support, use :kconfig:option:`CONFIG_NET_SOCKETS_ENABLE_DTLS` option. +.. _sockets_tls_credentials_subsys: + TLS credentials subsystem ========================= diff --git a/doc/connectivity/networking/api/system_mgmt.rst b/doc/connectivity/networking/api/system_mgmt.rst index 59aab16d3b72dfc..a09ed2b2787f279 100644 --- a/doc/connectivity/networking/api/system_mgmt.rst +++ b/doc/connectivity/networking/api/system_mgmt.rst @@ -18,3 +18,4 @@ Network System Management traffic-class.rst net_pkt_filter.rst net_shell.rst + tls_credentials_shell.rst diff --git a/doc/connectivity/networking/api/tls_credentials_shell.rst b/doc/connectivity/networking/api/tls_credentials_shell.rst new file mode 100644 index 000000000000000..69749a8f972b1f5 --- /dev/null +++ b/doc/connectivity/networking/api/tls_credentials_shell.rst @@ -0,0 +1,259 @@ +.. _tls_credentials_shell: + +TLS Credentials Shell +##################### + +The TLS Credentials shell provides a command-line interface for managing installed TLS credentials. + +Commands +******** + +.. _tls_credentials_shell_buf_cred: + +Buffer Credential (``buf``) +=========================== + +Buffer data incrementaly into the credential buffer so that it can be added using the :ref:`tls_credentials_shell_add_cred` command. + +Alternatively, clear the credential buffer. + +Usage +----- + +To append ```` to the credential buffer, use: + +.. code-block:: shell + + cred buf + +Use this as many times as needed to load the full credential into the credential buffer, then use the :ref:`tls_credentials_shell_add_cred` command to store it. + +To clear the credential buffer, use: + +.. code-block:: shell + + cred buf clear + +Arguments +--------- + +.. csv-table:: + :header: "Argument", "Description" + :widths: 15 85 + + "````", "Text data to be appended to credential buffer. It can be either text, or base64-encoded binary. See :ref:`tls_credentials_shell_add_cred` and :ref:`tls_credentials_shell_data_formats` for details." + +.. _tls_credentials_shell_add_cred: + +Add Credential (``add``) +========================= + +Add a TLS credential to the TLS Credential store. + +Credential contents can be provided in-line with the call to ``cred add``, or will otherwise be sourced from the credential buffer. + +Usage +----- + +To add a TLS credential using the data from the credential buffer, use: + +.. code-block:: shell + + cred add + +To add a TLS credential using data provided with the same command, use: + +.. code-block:: shell + + cred add + + +Arguments +--------- + +.. csv-table:: + :header: "Argument", "Description" + :widths: 15 85 + + "````", "The sectag to use for the new credential. Can be any non-negative integer." + "````", "The type of credential to add. See :ref:`tls_credentials_shell_cred_types` for valid values." + "````", "Reserved. Must always be ``DEFAULT`` (case-insensitive)." + "````", "Specifies the storage format of the provided credential. See :ref:`tls_credentials_shell_data_formats` for valid values." + "````", "If provided, this argument will be used as the credential data, instead of any data in the credential buffer. Can be either text, or base64-encoded binary." + +.. _tls_credentials_shell_del_cred: + +Delete Credential (``del``) +=========================== + +Delete a specified credential from the credential store. + +Usage +----- + +To delete a credential matching a specified sectag and credential type (if it exists), use: + +.. code-block:: shell + + cred del + +Arguments +--------- + +.. csv-table:: + :header: "Argument", "Description" + :widths: 15 85 + + "````", "The sectag of the credential to delete. Can be any non-negative integer." + "````", "The type of credential to delete. See :ref:`tls_credentials_shell_cred_types` for valid values." + +.. _tls_credentials_shell_get_cred: + +Get Credential Contents (``get``) +================================= + +Retrieve and print the contents of a specified credential. + +Usage +----- + +To retrieve and print a credential matching a specified sectag and credential type (if it exists), use: + +.. code-block:: shell + + cred get + +Arguments +--------- + +.. csv-table:: + :header: "Argument", "Description" + :widths: 15 85 + + "````", "The sectag of the credential to get. Can be any non-negative integer." + "````", "The type of credential to get. See :ref:`tls_credentials_shell_cred_types` for valid values." + "````", "Specifies the retrieval format for the provided credential. See :ref:`tls_credentials_shell_data_formats` for valid values." + +.. _tls_credentials_shell_list_cred: + +List Credentials (``list``) +=========================== + +List TLS credentials in the credential store. + +Usage +----- + +To list all available credentials, use: + +.. code-block:: shell + + cred list + +To list all credentials with a specified sectag, use: + +.. code-block:: shell + + cred list + +To list all credentials with a specified credential type, use: + +.. code-block:: shell + + cred list any + +To list all credentials with a specified credential type and sectag, use: + +.. code-block:: shell + + cred list + + +Arguments +--------- + +.. csv-table:: + :header: "Argument", "Description" + :widths: 15 85 + + "````", "Optional. If provided, only list credentials with this sectag. Pass ``any`` or omit to allow any sectag. Otherwise, can be any non-negative integer." + "````", "Optional. If provided, only list credentials with this credential type. Pass ``any`` or omit to allow any credential type. Otherwise, see :ref:`tls_credentials_shell_cred_types` for valid values." + + +Output +------ + +The command outputs all matching credentials in the following (CSV-compliant) format: + +.. code-block:: shell + + ,,, + +Where: + +.. csv-table:: + :header: "Symbol", "Value" + :widths: 15 85 + + "````", "The sectag of the listed credential. A non-negative integer." + "````", "Credential type short-code (see :ref:`tls_credentials_shell_cred_types` for details) of the listed credential." + "````", "A string digest representing the credential contents. The exact nature of this digest may vary depending on credentials storage backend, but currently for all backends this is a base64 encoded SHA256 hash of the raw credential contents (so different storage formats for essentially identical credentials will have different digests)." + "````", "Status code indicating success or failure with generating a digest of the listed credential. 0 if successful, negative error code specific to the storage backend otherwise. Lines for which status is not zero will be printed with error formatting." + +After the list is printed, a final summary of the found credentials will be printed in the form: + +.. code-block:: shell + + credentials found. + +Where `` is the number of credentials found, and is zero if none are found. + +.. _tls_credentials_shell_cred_types: + +Credential Types +**************** + +The following keywords (case-insensitive) may be used to specify a credential type: + +.. csv-table:: + :header: "Keyword(s)", "Meaning" + :widths: 15 85 + + "``CA_CERT``, ``CA``", "A trusted CA certificate." + "``SERVER_CERT``, ``SELF_CERT``, ``CLIENT_CERT``, ``CLIENT``, ``SELF``, ``SERV``", "Self or server certificate." + "``PRIVATE_KEY``, ``PK``", "A private key." + "``PRE_SHARED_KEY``, ``PSK``", "A pre-shared key." + "``PRE_SHARED_KEY_ID``, ``PSK_ID``", "ID for pre-shared key." + +.. _tls_credentials_shell_data_formats: + +Storage/Retrieval Formats +************************* + +The :ref:`tls_credentials ` module treats stored credentials as arbitrary binary buffers. + +For convenience, the TLS credentials shell offers four formats for providing and later retrieving these buffers using the shell. + +These formats and their (case-insensitive) keywords are as follows: + +.. csv-table:: + :header: "Keyword", "Meaning", "Behavior during storage (``cred add``)", "Behavior during retrieval (``cred get``)" + :widths: 3, 32, 34, 34 + + "``BIN``", "Credential is handled by shell as base64 and stored without NULL termination.", "Data entered into shell will be decoded from base64 into raw binary before storage. No terminator will be appended.", "Stored data will be encoded into base64 before being printed." + "``BINT``", "Credential is handled by shell as base64 and stored with NULL termination.", "Data entered into shell will be decoded from base64 into raw binary and a NULL terminator will be appended before storage.", "NULL terminator will be truncated from stored data before said data is encoded into base64 and then printed." + "``STR``", "Credential is handled by shell as literal string and stored without NULL termination.", "Text data entered into shell will be passed into storage as-written, without a NULL terminator.", "Stored data will be printed as text. Non-printable characters will be printed as ``?``" + "``STRT``", "Credential is handled by shell as literal string and stored with NULL-termination.", "Text data entered into shell will be passed into storage as-written, with a NULL terminator.", "NULL terminator will be truncated from stored data before said data is printed as text. Non-printable characters will be printed as ``?``" + +The ``BIN`` format can be used to install credentials of any type, since base64 can be used to encode any concievable binary buffer. +The remaining three formats are provided for convenience in special use-cases. + +For example: + +- To install printable pre-shared-keys, use ``STR`` to enter the PSK without first encoding it. + This ensures it is stored without a NULL terminator. +- To install DER-formatted X.509 certificates (or other raw-binary credentials, such as non-printable PSKs) base64-encode the binary and use the ``BIN`` format. +- To install PEM-formatted X.509 certificates or certificate chains, base64 encode the full PEM string (including new-lines and ``----BEGIN X ----`` / ``----END X----`` markers), and then use the ``BINT`` format to make sure the stored string is NULL-terminated. + This is required because Zephyr does not support multi-line strings in the shell. + Otherwise, the ``STRT`` format could be used for this purpose without base64 encoding. + It is possible to use ``BIN`` instead if you manually encode a NULL terminator into the base64. diff --git a/subsys/net/lib/tls_credentials/CMakeLists.txt b/subsys/net/lib/tls_credentials/CMakeLists.txt index dc08debc5faddb8..8acb4a5f4f104b3 100644 --- a/subsys/net/lib/tls_credentials/CMakeLists.txt +++ b/subsys/net/lib/tls_credentials/CMakeLists.txt @@ -10,3 +10,6 @@ zephyr_sources_ifdef(CONFIG_TLS_CREDENTIALS_BACKEND_PROTECTED_STORAGE tls_credentials_trusted.c tls_credentials_digest_raw.c ) +zephyr_sources_ifdef(CONFIG_TLS_CREDENTIALS_SHELL + tls_credentials_shell.c +) diff --git a/subsys/net/lib/tls_credentials/Kconfig b/subsys/net/lib/tls_credentials/Kconfig index 94ecc8287c6a62e..58ec0949d474f8c 100644 --- a/subsys/net/lib/tls_credentials/Kconfig +++ b/subsys/net/lib/tls_credentials/Kconfig @@ -54,4 +54,6 @@ config TLS_CREDENTIAL_FILENAMES This option is currently only available for secure socket offload devices. +source "subsys/net/lib/tls_credentials/Kconfig.shell" + endif # TLS_CREDENTIALS diff --git a/subsys/net/lib/tls_credentials/Kconfig.shell b/subsys/net/lib/tls_credentials/Kconfig.shell new file mode 100644 index 000000000000000..3e20f6e77174040 --- /dev/null +++ b/subsys/net/lib/tls_credentials/Kconfig.shell @@ -0,0 +1,35 @@ +# Copyright (c) 2023 Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +menuconfig TLS_CREDENTIALS_SHELL + bool "TLS credentials management shell" + depends on TLS_CREDENTIALS + depends on SHELL + depends on BASE64 + help + Enable shell commands to manage TLS credentials. + +if TLS_CREDENTIALS_SHELL + +config TLS_CREDENTIALS_SHELL_CRED_BUF_SIZE + int "Size of buffer used for storing and retrieving credentials, measured in bytes." + default 1024 + help + The amount of preallocated buffer (in bytes) used for storing and retrieving credentials. + +config TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH + int "Credential output line width (characters)" + default 32 + help + This setting specifies how long (in characters) contiguous lines of base64 credential + output should be. Must be a multiple of 4. Applies only to ??? mode. + +config TLS_CREDENTIALS_SHELL_DIGEST_BUF_SIZE + int "Buffer for generating credentials digests" + default 48 + help + The amount of preallocated buffer (in bytes) for temporarily storing credential digests. + + Also used to print error messages if digest generation fails. + +endif # TLS_CREDENTIALS_SHELL diff --git a/subsys/net/lib/tls_credentials/tls_credentials_shell.c b/subsys/net/lib/tls_credentials/tls_credentials_shell.c new file mode 100644 index 000000000000000..ca91a73135d15d2 --- /dev/null +++ b/subsys/net/lib/tls_credentials/tls_credentials_shell.c @@ -0,0 +1,811 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(tls_credentials_shell, CONFIG_TLS_CREDENTIALS_LOG_LEVEL); + +#include +#include +#include +#include +#include "tls_internal.h" +#include +#include +#include + +enum cred_storage_fmt { + /* Credential is stored as a string and will be passed between the shell and storage + * unmodified. + */ + CRED_STORAGE_FMT_STRING, + + /* Credential is stored as raw binary, and is parsed from base64 before storage and encoded + * back into base64 when retrieved via the shell. + */ + CRED_STORAGE_FMT_BINARY, +}; + +struct cred_type_string { + char *name; + enum tls_credential_type type; +}; + +/* The first entry in each credential type group will be used for human-readable shell + * output. The last will be used for compact shell output. The rest are accepted synonyms. + */ +static const struct cred_type_string type_strings[] = { + {"CA_CERT", TLS_CREDENTIAL_CA_CERTIFICATE}, + {"CA", TLS_CREDENTIAL_CA_CERTIFICATE}, + + {"SERVER_CERT", TLS_CREDENTIAL_SERVER_CERTIFICATE}, + {"CLIENT_CERT", TLS_CREDENTIAL_SERVER_CERTIFICATE}, + {"SELF_CERT", TLS_CREDENTIAL_SERVER_CERTIFICATE}, + {"SELF", TLS_CREDENTIAL_SERVER_CERTIFICATE}, + {"CLIENT", TLS_CREDENTIAL_SERVER_CERTIFICATE}, + {"SERV", TLS_CREDENTIAL_SERVER_CERTIFICATE}, + + {"PRIVATE_KEY", TLS_CREDENTIAL_PRIVATE_KEY}, + {"PK", TLS_CREDENTIAL_PRIVATE_KEY}, + + {"PRE_SHARED_KEY", TLS_CREDENTIAL_PSK}, + {"PSK", TLS_CREDENTIAL_PSK}, + + {"PRE_SHARED_KEY_ID", TLS_CREDENTIAL_PSK_ID}, + {"PSK_ID", TLS_CREDENTIAL_PSK_ID} +}; + +#define ANY_KEYWORD "any" + +/* This is so that we can output base64 in chunks of this length if necessary */ +BUILD_ASSERT( + (CONFIG_TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH % 4) == 0, + "CONFIG_TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH must be a multiple of 4." +); + +/* Output buffers used for printing credentials and digests. + * One extra byte included for NULL termination. + */ +static char cred_out_buf[CONFIG_TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH + 1]; +static char cred_digest_buf[CONFIG_TLS_CREDENTIALS_SHELL_DIGEST_BUF_SIZE + 1]; + +/* Internal buffer used for storing and retrieving credentials. + * +1 byte for potential NULL termination. + */ +static char cred_buf[CONFIG_TLS_CREDENTIALS_SHELL_CRED_BUF_SIZE + 1]; +static size_t cred_written; + +/* Some backends (namely, the volatile backend) store a reference rather than a copy of passed-in + * credentials. For these backends, we need to copy incoming credentials onto the heap before + * attempting to store them. + * + * Since the backend in use is determined at build time by KConfig, so is this behavior. + * If multi/dynamic-backend support is ever added, this will need to be updated. + */ +#define COPY_CREDENTIALS_TO_HEAP CONFIG_TLS_CREDENTIALS_BACKEND_VOLATILE + +/* Used to track credentials that have been copied permanently to the heap, in case they are + * ever deleted and need to be freed. + */ +static void *cred_refs[CONFIG_TLS_MAX_CREDENTIALS_NUMBER]; + +/* Find an empty slot in the cred_refs array, or return -1 if none exists. + * Pass NULL to find an unused slot. + */ +static int find_ref_slot(const void *const cred) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(cred_refs); i++) { + if (cred_refs[i] == cred) { + return i; + } + } + + return -1; +} + +/* Helpers */ + +/* Filter out non-printable characters from a passed-in string of known length */ +static int filter_nonprint(char *buf, size_t len, char inval) +{ + int i; + int ret = 0; + + for (i = 0; i < len; i++) { + if (!isprint((int)buf[i])) { + buf[i] = inval; + ret = -EINVAL; + } + } + + return ret; +} + +/* Verify that a provided string consists only of the characters 0-9*/ +static bool check_numeric(char *str) +{ + int i; + int len = strlen(str); + + for (i = 0; i < len; i++) { + if (!isdigit((int)str[i])) { + return false; + } + } + + return true; +} + +/* Clear the credential write buffer, returns true if anything was actually cleared. */ +static bool cred_buf_clear(void) +{ + bool cleared = cred_written != 0; + + (void)memset(cred_buf, 0, sizeof(cred_buf)); + cred_written = 0; + + return cleared; +} + +/* Parse a (possibly incomplete) chunk into the credential buffer */ +static int cred_buf_write(char *chunk) +{ + char *writehead = cred_buf + cred_written; + size_t chunk_len = strlen(chunk); + + /* Verify that there is room for the incoming chunk */ + if ((writehead + chunk_len) >= (cred_buf + sizeof(cred_buf) - 1)) { + return -ENOMEM; + } + + /* Append chunk to the credential buffer. + * Deliberately do not copy NULL terminator. + */ + memcpy(writehead, chunk, chunk_len); + cred_written += chunk_len; + + return chunk_len; +} + +/* Get the human-readable name of a TLS credential type */ +static const char *cred_type_name(enum tls_credential_type type) +{ + /* Scan over predefined type strings, and return the name + * of the first one of matching type. + */ + for (int i = 0; i < ARRAY_SIZE(type_strings); i++) { + if (type_strings[i].type == type) { + return type_strings[i].name; + } + } + + /* No matches found, it's invalid. */ + return "INVALID"; +} + +/* Get the compact name of a TLS credential type*/ +static const char *cred_type_name_compact(enum tls_credential_type type) +{ + /* Scan over predefined type strings, and return the name + * of the last one of matching type. + */ + for (int i = ARRAY_SIZE(type_strings) - 1; i >= 0; i--) { + if (type_strings[i].type == type) { + return type_strings[i].name; + } + } + + /* No matches found, it's invalid. */ + return "INV"; +} + +/* Shell interface routines */ + +/* Attempt to parse a command line argument into a sectag. + * TLS_SEC_TAG_NONE is returned if ANY_KEYWORD is provided. + */ +static int shell_parse_cred_sectag(const struct shell *sh, char *arg, sec_tag_t *out, + bool allow_any) +{ + unsigned long sectag_value; + int err = 0; + + /* Check for "ANY" special keyword if desired. */ + if (allow_any && strcasecmp(arg, ANY_KEYWORD) == 0) { + *out = TLS_SEC_TAG_NONE; + return 0; + } + + /* Otherwise, verify that the sectag is purely numeric */ + if (!check_numeric(arg)) { + err = -EINVAL; + goto error; + } + + /* Use strtoul because it has nicer validation features than atoi */ + sectag_value = shell_strtoul(arg, 10, &err); + + if (!err) { + *out = (sec_tag_t)sectag_value; + return 0; + } + +error: + shell_fprintf(sh, SHELL_ERROR, "%s is not a valid sectag.\n", arg); + return err; +} + +/* Attempt to parse a command line argument into a credential type. + * TLS_CREDENTIAL_NONE is returned if ANY_KEYWORD is provided. + */ +static int shell_parse_cred_type(const struct shell *sh, char *arg, enum tls_credential_type *out, + bool allow_any) +{ + /* Check for "ANY" special keyword if desired. */ + if (allow_any && strcasecmp(arg, ANY_KEYWORD) == 0) { + *out = TLS_CREDENTIAL_NONE; + return 0; + } + + /* Otherwise, scan over predefined type strings, and return the corresponding + * credential type if one is found + */ + for (int i = 0; i < ARRAY_SIZE(type_strings); i++) { + if (strcasecmp(arg, type_strings[i].name) == 0) { + *out = type_strings[i].type; + return 0; + } + } + + /* No matches found, it's invalid. */ + shell_fprintf(sh, SHELL_ERROR, "%s is not a valid credential type.\n", arg); + + return -EINVAL; +} + +/* Parse a backend specifier argument + * Right now, only a single backend is supported, so this is serving simply as a reserved argument. + * As such, the only valid input is "default" + */ +static int shell_parse_cred_backend(const struct shell *sh, char *arg) +{ + if (strcasecmp(arg, "default") == 0) { + return 0; + } + + shell_fprintf(sh, SHELL_ERROR, "%s is not a valid backend.\n", arg); + + return -EINVAL; +} + +/* Parse an input type specifier */ +static int shell_parse_cred_storage_format(const struct shell *sh, char *arg, + enum cred_storage_fmt *out, bool *terminated) +{ + if (strcasecmp(arg, "bin") == 0) { + *out = CRED_STORAGE_FMT_BINARY; + *terminated = false; + return 0; + } + + if (strcasecmp(arg, "bint") == 0) { + *out = CRED_STORAGE_FMT_BINARY; + *terminated = true; + return 0; + } + + if (strcasecmp(arg, "str") == 0) { + *out = CRED_STORAGE_FMT_STRING; + *terminated = false; + return 0; + } + + if (strcasecmp(arg, "strt") == 0) { + *out = CRED_STORAGE_FMT_STRING; + *terminated = true; + return 0; + } + + shell_fprintf(sh, SHELL_ERROR, "%s is not a valid storage format.\n", arg); + + return -EINVAL; +} + +/* Clear credential buffer, with shell feedback */ +static void shell_clear_cred_buf(const struct shell *sh) +{ + /* We will only print a message if some data was actually wiped. */ + if (cred_buf_clear()) { + shell_fprintf(sh, SHELL_NORMAL, "Credential buffer cleared.\n"); + } +} + +/* Write data into the credential buffer, with shell feedback. */ +static int shell_write_cred_buf(const struct shell *sh, char *chunk) +{ + int res = cred_buf_write(chunk); + + /* Report results. */ + + if (res == -ENOMEM) { + shell_fprintf(sh, SHELL_ERROR, "Not enough room in credential buffer for " + "provided data. Increase " + "CONFIG_TLS_CREDENTIALS_SHELL_CRED_BUF_SIZE.\n"); + shell_clear_cred_buf(sh); + return -ENOMEM; + } + + shell_fprintf(sh, SHELL_NORMAL, "Stored %d bytes.\n", res); + + return 0; +} + +/* Adds a credential to the credential store */ +static int tls_cred_cmd_add(const struct shell *sh, size_t argc, char *argv[]) +{ + int err = 0; + sec_tag_t sectag; + enum cred_storage_fmt format; + bool terminated; + enum tls_credential_type type; + void *cred_copy = NULL; + void *cred_chosen = NULL; + bool keep_copy = false; + int ref_slot = -1; + + /* Lock credentials so that we can interact with them directly. + * Mainly this is required by credential_get. + */ + + credentials_lock(); + + err = shell_parse_cred_sectag(sh, argv[1], §ag, false); + if (err) { + goto cleanup; + } + + err = shell_parse_cred_type(sh, argv[2], &type, false); + if (err) { + goto cleanup; + } + + err = shell_parse_cred_backend(sh, argv[3]); + if (err) { + goto cleanup; + } + + err = shell_parse_cred_storage_format(sh, argv[4], &format, &terminated); + if (err) { + goto cleanup; + } + + if (argc == 6) { + /* Credential was passed, clear credential buffer and then use the passed-in + * credential. + */ + shell_clear_cred_buf(sh); + err = shell_write_cred_buf(sh, argv[5]); + if (err) { + goto cleanup; + } + } + + /* Make sure the credential buffer isn't empty. */ + if (cred_written == 0) { + shell_fprintf(sh, SHELL_ERROR, "Please provide a credential to add.\n"); + err = -ENOENT; + goto cleanup; + } + + /* Check whether a credential of this type and sectag already exists. */ + if (credential_get(sectag, type)) { + shell_fprintf(sh, SHELL_ERROR, "TLS credential with sectag %d and type %s " + "already exists.\n", sectag, cred_type_name(type)); + err = -EEXIST; + goto cleanup; + } + + /* If binary format was specified, decode from base64. */ + if (format == CRED_STORAGE_FMT_BINARY) { + /* base64_decode can handle in-place operation. + * Pass &cred_written as olen so that it is updated to match the size of the base64 + * encoding. + * + * We use sizeof(cred_buf) - 1 since we want to keep room fors a NULL terminator. + * Though, technically, this is not actually needed since the output of + * base64_decode is always shorter than its input. + */ + err = base64_decode(cred_buf, sizeof(cred_buf) - 1, &cred_written, + cred_buf, cred_written); + if (err) { + shell_fprintf(sh, SHELL_ERROR, "Could not decode input from base64, " + "error: %d\n", err); + err = -EINVAL; + goto cleanup; + } + } + + /* If NULL termination was requested, append one. + * We are always guaranteed to have room in the buffer for this. + */ + if (terminated) { + cred_buf[cred_written] = 0; + cred_written += 1; + } + + /* Default to using cred_buf directly. */ + cred_chosen = cred_buf; + + /* If the currently active TLS Credentials backend stores credentials by reference, + * copy the incoming credentials off to the heap, and then use this copy instead. + */ + if (IS_ENABLED(COPY_CREDENTIALS_TO_HEAP)) { + /* Before copying the credential to heap, make sure we are able to store a + * reference to it so that it can be freed if the credential is ever deleted. + */ + + ref_slot = find_ref_slot(NULL); + + if (ref_slot < 0) { + shell_fprintf(sh, SHELL_ERROR, "No reference slot available, cannot copy " + "credential to heap. Credential will not be " + "stored\n"); + err = -ENOMEM; + goto cleanup; + } + + cred_copy = k_malloc(cred_written); + if (!cred_copy) { + shell_fprintf(sh, SHELL_ERROR, "Not enough heap for TLS credential of " + "size %d.\n", cred_written); + err = -ENOMEM; + goto cleanup; + } + + memset(cred_copy, 0, cred_written); + memcpy(cred_copy, cred_buf, cred_written); + + shell_fprintf(sh, SHELL_WARNING, "Credential has been copied to heap. Memory will " + "be leaked if this credential is deleted without " + "using the shell.\n"); + + cred_chosen = cred_copy; + } + + /* Finally, store the credential in whatever credentials backend is active. */ + err = tls_credential_add(sectag, type, cred_chosen, cred_written); + if (err) { + shell_fprintf(sh, SHELL_ERROR, "Failed to add TLS credential with sectag %d and " + "type %s. Error: %d\n.", sectag, + cred_type_name(type), err); + goto cleanup; + } + + /* Do not free the copied key during cleanup, since it was successfully written. */ + keep_copy = true; + + shell_fprintf(sh, SHELL_NORMAL, "Added TLS credential of type %s, sectag %d, and length %d " + "bytes.\n", cred_type_name(type), sectag, cred_written); + +cleanup: + /* Unlock credentials since we are done interacting with internal state. */ + credentials_unlock(); + + /* We are also done with the credentials buffer, so clear it for good measure. */ + shell_clear_cred_buf(sh); + + /* If we copied the credential, make sure it is eventually freed. */ + if (cred_copy) { + if (keep_copy) { + /* If the credential was successfully stored, keep a reference to it in case + * it is ever deleted and needs to be freed. + */ + cred_refs[ref_slot] = cred_copy; + } else { + /* Otherwise, clear and free it immediately */ + memset(cred_copy, 0, cred_written); + (void)k_free(cred_copy); + } + } + + return err; +} + +/* Buffers credential data into the credential buffer. */ +static int tls_cred_cmd_buf(const struct shell *sh, size_t argc, char *argv[]) +{ + /* If the "clear" keyword is provided, clear the buffer rather than write to it. */ + if (strcmp(argv[1], "clear") == 0) { + shell_clear_cred_buf(sh); + return 0; + } + + /* Otherwise, assume provided arg is base64 and attempt to write it into the credential + * buffer. + */ + return shell_write_cred_buf(sh, argv[1]); +} + +/* Deletes a credential from the credential store */ +static int tls_cred_cmd_del(const struct shell *sh, size_t argc, char *argv[]) +{ + int err = 0; + sec_tag_t sectag; + enum tls_credential_type type; + struct tls_credential *cred = NULL; + int ref_slot = -1; + + /* Lock credentials so that we can safely use internal access functions. */ + credentials_lock(); + + err = shell_parse_cred_sectag(sh, argv[1], §ag, false); + if (err) { + goto cleanup; + } + + err = shell_parse_cred_type(sh, argv[2], &type, false); + if (err) { + goto cleanup; + } + + /* Check whether a credential of this type and sectag actually exists. */ + cred = credential_get(sectag, type); + if (!cred) { + shell_fprintf(sh, SHELL_ERROR, "There is no TLS credential with sectag %d and " + "type %s.\n", sectag, cred_type_name(type)); + err = -ENOENT; + goto cleanup; + } + + ref_slot = find_ref_slot(cred->buf); + if (ref_slot >= 0) { + /* This was a credential we copied to heap. Clear and free it. */ + memset((void*)cred_buf, 0, cred->len); + k_free((void*)cred_buf); + cred->buf = NULL; + + /* Clear the reference slot so it can be used again. */ + cred_refs[ref_slot] = NULL; + + shell_fprintf(sh, SHELL_NORMAL, "Stored credential freed.\n"); + } + + /* Attempt to delete. */ + err = tls_credential_delete(sectag, type); + if (err) { + shell_fprintf(sh, SHELL_ERROR, "Deleting TLS credential with sectag %d and " + "type %s failed with error: %d.\n", sectag, + cred_type_name(type), err); + goto cleanup; + } + + shell_fprintf(sh, SHELL_NORMAL, "Deleted TLS credential with sectag %d and type %s.\n", + sectag, cred_type_name(type)); + +cleanup: + /* Unlock credentials since we are done interacting with internal state. */ + credentials_unlock(); + + return err; +} + +/* Retrieves credential data from credential store. */ +static int tls_cred_cmd_get(const struct shell *sh, size_t argc, char *argv[]) +{ + int i; + int remaining; + int written; + int err = 0; + size_t cred_len; + sec_tag_t sectag; + enum tls_credential_type type; + enum cred_storage_fmt format; + bool terminated; + + size_t line_length; + + /* Lock credentials so that we can safely use internal access functions. */ + credentials_lock(); + + err = shell_parse_cred_sectag(sh, argv[1], §ag, false); + if (err) { + goto cleanup; + } + + err = shell_parse_cred_type(sh, argv[2], &type, false); + if (err) { + goto cleanup; + } + + err = shell_parse_cred_storage_format(sh, argv[3], &format, &terminated); + if (err) { + goto cleanup; + } + + line_length = CONFIG_TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH; + + /* If the credential is stored as binary, adjust line length so that the output + * base64 has width CONFIG_TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH + */ + if (format == CRED_STORAGE_FMT_BINARY) { + line_length = CONFIG_TLS_CREDENTIALS_SHELL_CRED_OUTPUT_WIDTH / 4 * 3; + } + + /* Check whether a credential of this type and sectag actually exists. */ + if (!credential_get(sectag, type)) { + shell_fprintf(sh, SHELL_ERROR, "There is no TLS credential with sectag %d and " + "type %s.\n", sectag, cred_type_name(type)); + err = -ENOENT; + goto cleanup; + } + + /* Clear the credential buffer before use. */ + shell_clear_cred_buf(sh); + + /* Load the credential into the credential buffer */ + cred_len = sizeof(cred_buf); + err = tls_credential_get(sectag, type, cred_buf, &cred_len); + if (err == -EFBIG) { + shell_fprintf(sh, SHELL_ERROR, "Not enough room in the credential buffer to " + "retrieve credential with sectag %d and type %s. " + "Increase TLS_CREDENTIALS_SHELL_MAX_CRED_LEN.\n", + sectag, cred_type_name(type)); + err = -ENOMEM; + goto cleanup; + } else if (err) { + shell_fprintf(sh, SHELL_ERROR, "Could not retrieve TLS credential with sectag %d " + "and type %s due to error: %d.\n", sectag, + cred_type_name(type), err); + goto cleanup; + } + + /* Update the credential buffer writehead. + * Keeping this accurate ensures that a "Buffer Cleared" message is eventually printed. + */ + cred_written = cred_len; + + /* If the stored credential is NULL-terminated, do not include NULL termination in output */ + if (terminated) { + if (cred_buf[cred_written - 1] != 0) { + shell_fprintf(sh, SHELL_ERROR, "The stored credential isn't " + "NULL-terminated, but a NULL-terminated " + "format was specified.\n"); + + err = -EINVAL; + goto cleanup; + } + cred_written -= 1; + } + + /* Print the credential out in lines. */ + for (i = 0; i < cred_written; i += line_length) { + /* Print either a full line, or however much credential data is left. */ + remaining = MIN(line_length, cred_written - i); + + /* Read out a line of data. */ + memset(cred_out_buf, 0, sizeof(cred_out_buf)); + if (format == CRED_STORAGE_FMT_BINARY) { + (void)base64_encode(cred_out_buf, sizeof(cred_out_buf), + &written, &cred_buf[i], remaining); + } else if (format == CRED_STORAGE_FMT_STRING) { + memcpy(cred_out_buf, &cred_buf[i], remaining); + if (filter_nonprint(cred_out_buf, remaining, '?')) { + err = -EBADF; + } + } + + /* Print the line. */ + shell_fprintf(sh, SHELL_NORMAL, "%s\n", cred_out_buf); + } + + if (err) { + shell_fprintf(sh, SHELL_WARNING, "Non-printable characters were included in the " + "output and filtered. Have you selected the " + "correct storage format?\n"); + } + +cleanup: + /* Unlock credentials since we are done interacting with internal state. */ + credentials_unlock(); + + /* Clear buffers when done. */ + memset(cred_out_buf, 0, sizeof(cred_out_buf)); + shell_clear_cred_buf(sh); + + return err; +} + +/* Lists credentials in credential store. */ +static int tls_cred_cmd_list(const struct shell *sh, size_t argc, char *argv[]) +{ + int err = 0; + size_t digest_size; + sec_tag_t sectag = TLS_SEC_TAG_NONE; + struct tls_credential *cred; + int count = 0; + + sec_tag_t sectag_filter = TLS_SEC_TAG_NONE; + enum tls_credential_type type_filter = TLS_CREDENTIAL_NONE; + + /* Lock credentials so that we can safely use internal access functions. */ + credentials_lock(); + + /* Sectag filter was provided, parse it. */ + if (argc >= 2) { + err = shell_parse_cred_sectag(sh, argv[1], §ag_filter, true); + if (err) { + goto cleanup; + } + } + + /* Credential type filter was provided, parse it. */ + if (argc >= 3) { + err = shell_parse_cred_type(sh, argv[2], &type_filter, true); + if (err) { + goto cleanup; + } + } + + /* Scan through all occupied sectags */ + while ((sectag = credential_next_tag_get(sectag)) != TLS_SEC_TAG_NONE) { + /* Filter by sectag if requested. */ + if (sectag_filter != TLS_SEC_TAG_NONE && sectag != sectag_filter) { + continue; + } + + cred = NULL; + /* Scan through all credentials within each sectag */ + while ((cred = credential_next_get(sectag, cred)) != NULL) { + /* Filter by credential type if requested. */ + if (type_filter != TLS_CREDENTIAL_NONE && cred->type != type_filter) { + continue; + } + count++; + + /* Generate a digest of the credential */ + memset(cred_digest_buf, 0, sizeof(cred_digest_buf)); + strcpy(cred_digest_buf, "N/A"); + digest_size = sizeof(cred_digest_buf); + err = credential_digest(cred, cred_digest_buf, &digest_size); + + /* Print digest and sectag/type info */ + shell_fprintf(sh, err ? SHELL_ERROR : SHELL_NORMAL, "%d,%s,%s,%d\n", + sectag, cred_type_name_compact(cred->type), + err ? "ERROR" : cred_digest_buf, err); + + err = 0; + } + }; + + shell_fprintf(sh, SHELL_NORMAL, "%d credentials found.\n", count); + +cleanup: + /* Unlock credentials since we are done interacting with internal state. */ + credentials_unlock(); + + /* Clear digest buffer afterwards for good measure. */ + memset(cred_digest_buf, 0, sizeof(cred_digest_buf)); + + return 0; +} + +SHELL_STATIC_SUBCMD_SET_CREATE(tls_cred_cmds, + SHELL_CMD_ARG(buf, NULL, "Buffer in credential data so it can be added.", + tls_cred_cmd_buf, 2, 0), + SHELL_CMD_ARG(add, NULL, "Add a TLS credential.", + tls_cred_cmd_add, 5, 1), + SHELL_CMD_ARG(del, NULL, "Delete a TLS credential.", + tls_cred_cmd_del, 3, 0), + SHELL_CMD_ARG(get, NULL, "Retrieve the contents of a TLS credential", + tls_cred_cmd_get, 4, 0), + SHELL_CMD_ARG(list, NULL, "List stored TLS credentials, optionally filtering by type " + "or sectag.", + tls_cred_cmd_list, 1, 2), + SHELL_SUBCMD_SET_END +); + +SHELL_CMD_REGISTER(cred, &tls_cred_cmds, "TLS Credentials Commands", NULL);