10.10. Bootstrapper and SIM bootstrap

10.10.1. General description

The LwM2M specification defines Bootstrap from Smartcard, a mode of bootstrapping the device where the initial Bootstrap Information is stored on a smart card - typically the SIM card in case of devices that use cellular connectivity.

Standard file formats for this bootstrap information and related metadata are defined in Appendix G, of the LwM2M Technical Specification, and specifications for the secure channel between Smartcard and LwM2M Device Storage in Appendix H thereof.

The “bootstrapper” feature, available as a commercial extension to the Anjay library, includes two modules that aid in implementing this part of the specification:

With the above features in place, all that’s left to implement is actual communication with the smart card, typically sending and receiving AT+CSIM commands to a cellular modem.

Bootstrapping from smart card has a number of advantages, including:

  • Ability to store bootstrap information securely, increasing the device’s resilience against tampering

  • Possibility to remotely update bootstrap information using cellular infrastructure, without the need for a full firmware upgrade

  • For devices controlled by cellular carriers - ability to control the bootstrap information without contacting the device manufacturer

10.10.2. Technical documentation

10.10.2.1. Enabling the bootstrapper module

If the bootstrapper feature is available in your version of Anjay, it can be enabled at compile time by enabling the ANJAY_WITH_MODULE_BOOTSTRAPPER macro in the anjay_config.h file or, if using CMake, enabling the corresponding WITH_MODULE_bootstrapper CMake option.

When this feature is enabled, the anjay_bootstrapper() function can be used. The user will need to provide an implementation of avs_stream_t that allows the Anjay code to read the file contained on the smartcard. The avs_stream_simple_input_create() function from the avs_stream_simple_io.h header is likely to be the easiest way to provide such an implementation, aside from using the SIM bootstrap module described below.

10.10.2.2. Enabling and configuring the sim_bootstrap module

Similarly, to enable the sim_bootstrap module, you can enable the ANJAY_WITH_MODULE_SIM_BOOTSTRAP macro in the anjay_config.h file or, if using CMake, enable the corresponding WITH_MODULE_sim_bootstrap CMake option. This requires that the bootstrapper feature is also enabled.

By default, the module will access the PKCS#15 application directory file and search it for the EF(DODF-bootstrap) file in a way that is compliant with LwM2M TS Appendix G mentioned above.

However, you can override the OID of the file to look for, by defining the ANJAY_MODULE_SIM_BOOTSTRAP_DATA_OID_OVERRIDE_HEX macro in anjay_config.h or setting the corresponding MODULE_sim_bootstrap_DATA_OID_OVERRIDE_HEX CMake option. It shall be set to a string containing hexlified DER representation of the desired OID. The default, standards-compliant value is "672b0901" (which corresponds to OID 2.23.43.9.1), but you may need to change it to a different value, for example some cards are known to use a mistakenly encoded value of "0604672b0901".

Alternatively, you might define the ANJAY_MODULE_SIM_BOOTSTRAP_HARDCODED_FILE_ID macro (or set the MODULE_sim_bootstrap_HARDCODED_FILE_ID CMake option) to bypass the directory search entirely and set a hardcoded file ID, e.g. 0x6432.

Once the module is enabled and configured, you can use the anjay_sim_bootstrap_stream_create() function to create an input stream suitable for passing to anjay_bootstrapper(). In the simplest case, you can also use the anjay_sim_bootstrap_perform() function that combines both calls and automatically closes the stream as well.

10.10.2.3. Bootstrap information generator tool

The generator.py application, located in the bootstrap directory of Anjay source package, allows generating binary files in the EF LwM2M_Bootstrap format that is supposed to be stored on smart cards, from a human-readable text file format.

The generator.py script, by default, processes the standard input and outputs to the standard output. However, an input file may be specified using the -c option, and the output file may be specified using the -o option.

Warning

The generator script is NOT intended to be safe to use with arbitrary input data. It is only intended for convenience when working with files created locally by trusted parties.

The input text files are evaluated as Python code, and as such, running the generator script with untrusted input may lead to arbitrary operations being performed on the computer.

The input file shall specify a dictionary according to Python syntax where:

  • On the top level, the keys shall be Object IDs, and the values shall be nested dictionaries describing the objects.

  • On the Object level, the keys shall be Instance IDs, and the values shall be nested dictionaries describing the instances.

  • On the Object Instance level, the keys shall be Resource IDs, and the values shall be either of:

    • Primitive types (numbers, booleans, strings or bytes objects) for single-instance resources

    • Lists of pairs (tuples of length 2) for Multiple-Instance Resources - in that case the first pair element shall be the Resource Instance ID, and the second one shall be the value of a primitive type

The constants from the OID and RID objects, as defined in the tests/integration/framework/test_utils.py file, may be used to make the keys more descriptive, as in the example input file (bootstrap/configs/basic):

{
    OID.Security: {
        1: {
            RID.Security.ServerURI          : 'coaps://eu.iot.avsystem.cloud:5684',
            RID.Security.Bootstrap          : False,
            RID.Security.Mode               : 0,  # PSK
            RID.Security.PKOrIdentity       : b'example-psk-identity',
            RID.Security.SecretKey          : b'3x@mpl3P5K53cr3tK3y',
            RID.Security.ShortServerID      : 1
        },
    },

    OID.Server: {
        1: {
            RID.Server.ShortServerID        : 1,
            RID.Server.Lifetime             : 86400,
            RID.Server.NotificationStoring  : False,
            RID.Server.Binding              : 'U'
        },
    }
}

The above example is equivalent to the following data written only using primitive values:

{
    0: {
        1: {
            0: 'coaps://eu.iot.avsystem.cloud:5684',
            1: False,
            2: 0,
            3: b'example-psk-identity',
            5: b'3x@mpl3P5K53cr3tK3y',
            10: 1
        }
    },
    1: {
        1: {
            0: 1,
            1: 86400,
            6: False,
            7: 'U'
        }
    }
}

The following example shell session illustrates the way of generating the binary bootstrap information file:

~/projects/anjay/bootstrap$ ./generator.py -c configs/basic -o basic_config.dat
~/projects/anjay/bootstrap$ hexdump -C basic_config.dat
00000000  00 02 00 7a 00 00 00 00  5e 08 01 5b c8 00 22 63  |...z....^..[.."c|
00000010  6f 61 70 73 3a 2f 2f 65  75 2e 69 6f 74 2e 61 76  |oaps://eu.iot.av|
00000020  73 79 73 74 65 6d 2e 63  6c 6f 75 64 3a 35 36 38  |system.cloud:568|
00000030  34 c1 01 00 c1 02 00 c8  03 14 65 78 61 6d 70 6c  |4.........exampl|
00000040  65 2d 70 73 6b 2d 69 64  65 6e 74 69 74 79 c8 05  |e-psk-identity..|
00000050  13 33 78 40 6d 70 6c 33  50 35 4b 35 33 63 72 33  |.3x@mpl3P5K53cr3|
00000060  74 4b 33 79 c1 0a 01 00  01 00 00 12 08 01 0f c1  |tK3y............|
00000070  00 01 c4 01 00 01 51 80  c1 06 00 c1 07 55        |......Q......U|
0000007e

10.10.2.4. Example code

Note

The full code for the following example can be found in the examples/commercial-features/CF-SmartCardBootstrap directory in Anjay sources. Note that to compile and run it, you need to have access to a commercial version of Anjay that includes the bootstrapper feature.

The example is loosely based on the Installing mandatory Objects tutorial, and additionally borrows much of the modem communication code from Non-IP Data Delivery. Since the bootstrap information will be loaded from a smart card, the setup_security_object() and setup_server_object() functions are no longer necessary, and the calls to them can be replaced with direct calls to anjay_security_object_install() and anjay_server_object_install():

int main(int argc, char *argv[]) {
    if (argc != 3) {
        avs_log(tutorial, ERROR, "usage: %s ENDPOINT_NAME MODEM_PATH", argv[0]);
        return -1;
    }

    const anjay_configuration_t CONFIG = {
        .endpoint_name = argv[1],
        .in_buffer_size = 4000,
        .out_buffer_size = 4000,
        .msg_cache_size = 4000
    };

    anjay_t *anjay = anjay_new(&CONFIG);
    if (!anjay) {
        avs_log(tutorial, ERROR, "Could not create Anjay object");
        return -1;
    }

    int result = 0;
    // Setup necessary objects
    if (anjay_security_object_install(anjay)
            || anjay_server_object_install(anjay)) {
        result = -1;
    }

    if (!result) {
        result = bootstrap_from_sim(anjay, argv[2]);
    }

    if (!result) {
        result = anjay_event_loop_run(
                anjay, avs_time_duration_from_scalar(1, AVS_TIME_S));
    }

    anjay_delete(anjay);
    return result;
}

As you can see, the command line now expects a second argument with a name of the file containing the bootstrap information.

This file is loaded using the bootstrap_from_sim() function, implemented as follows:

typedef struct {
    avs_buffer_t *buffer;
} fifo_t;

// ...

typedef struct {
    fifo_t fifo;
    int pts_fd;
} modem_ctx_t;

// ...

static int sim_perform_command(void *modem_ctx_,
                               const void *cmd,
                               size_t cmd_length,
                               void *out_buf,
                               size_t out_buf_size,
                               size_t *out_response_size) {
    modem_ctx_t *modem_ctx = (modem_ctx_t *) modem_ctx_;
    char req_buf[REQ_BUF_SIZE];
    char resp_buf[RESP_BUF_SIZE] = "";

    char *req_buf_ptr = req_buf;
    char *const req_buf_end = req_buf + sizeof(req_buf);
    int result = avs_simple_snprintf(req_buf_ptr,
                                     (size_t) (req_buf_end - req_buf_ptr),
                                     "AT+CSIM=%" PRIu32 ",\"",
                                     (uint32_t) (2 * cmd_length));
    if (result < 0) {
        return result;
    }
    req_buf_ptr += result;
    if ((size_t) (req_buf_end - req_buf_ptr) < 2 * cmd_length) {
        return -1;
    }
    if ((result = avs_hexlify(req_buf_ptr, (size_t) (req_buf_end - req_buf_ptr),
                              NULL, cmd, cmd_length))) {
        return result;
    }
    req_buf_ptr += 2 * cmd_length;
    if ((result = avs_simple_snprintf(
                 req_buf_ptr, (size_t) (req_buf_end - req_buf_ptr), "\"\r\n"))
            < 0) {
        return result;
    }
    req_buf_ptr += result;
    ssize_t written =
            write(modem_ctx->pts_fd, req_buf, (size_t) (req_buf_ptr - req_buf));
    if (written != (ssize_t) (req_buf_ptr - req_buf)) {
        return -1;
    }
    avs_time_monotonic_t deadline = avs_time_monotonic_add(
            avs_time_monotonic_now(),
            avs_time_duration_from_scalar(5, AVS_TIME_S));
    bool csim_resp_received = false;
    bool ok_received = false;
    while (!ok_received) {
        if (modem_getline(modem_ctx, resp_buf, sizeof(resp_buf), deadline)) {
            return -1;
        }
        const char *resp_terminator = memchr(resp_buf, '\0', sizeof(resp_buf));
        if (!resp_terminator) {
            return -1;
        }
        if (memcmp(resp_buf, CSIM_RESP, strlen(CSIM_RESP)) == 0) {
            if (csim_resp_received) {
                return -1;
            }
            errno = 0;
            char *endptr = NULL;
            long long resp_reported_length =
                    strtoll(resp_buf + strlen(CSIM_RESP), &endptr, 10);
            if (errno || !endptr || endptr[0] != ',' || endptr[1] != '"'
                    || resp_reported_length < 0
                    || endptr + resp_reported_length + 2 >= resp_terminator
                    || endptr[resp_reported_length + 2] != '"'
                    || avs_unhexlify(out_response_size, (uint8_t *) out_buf,
                                     out_buf_size, endptr + 2,
                                     (size_t) resp_reported_length)) {
                return -1;
            }
            csim_resp_received = true;
        } else if (strcmp(resp_buf, "OK") == 0) {
            ok_received = true;
        }
    }
    return csim_resp_received ? 0 : -1;
}

static int bootstrap_from_sim(anjay_t *anjay, const char *modem_device) {
    modem_ctx_t modem_ctx = {
        .pts_fd = -1
    };
    int result = -1;

    avs_log(tutorial, INFO, "Attempting to bootstrap from SIM card");

    if (fifo_init(&modem_ctx.fifo)) {
        avs_log(tutorial, ERROR, "could not initialize FIFO");
        goto finish;
    }
    if ((modem_ctx.pts_fd = open(modem_device, O_RDWR)) < 0) {
        avs_log(tutorial, ERROR, "could not open modem device %s: %s",
                modem_device, strerror(errno));
        goto finish;
    }
    if (avs_is_err(anjay_sim_bootstrap_perform(anjay, sim_perform_command,
                                               &modem_ctx))) {
        avs_log(tutorial, ERROR, "Could not bootstrap from SIM card");
        goto finish;
    }
    result = 0;
finish:
    if (modem_ctx.pts_fd >= 0) {
        close(modem_ctx.pts_fd);
    }
    fifo_destroy(&modem_ctx.fifo);
    return result;
}

The sim_perform_command() function is a callback that is passed to the sim_bootstrap module logic, and performs the AT+CSIM command over a serial port. The modem_getline() function it calls is almost identical to the one originally implemented for Non-IP Data Delivery.

The bootstrap_from_sim() function itself is a wrapper over anjay_sim_bootstrap_perform() that additionally initializes and closes the card communication channel.